In certain scenarios an attempt to iterate over JavaScript Map keys or values fails. The script simply doesn't enter the loop.
function pushAndLog(array, values) {
array.push(...values);
for (const value of values) {
console.log(value);
}
}
const array = [];
const map = new Map([["a", 1], ["b", 2]]);
pushAndLog(array, map.keys());
In this example, array
gets properly updated, but map keys don't get logged.
This happens because Map.keys() and Map.values() methods return an Iterator instead of an Iterable object.
As opposed to Iterable, Iterator is a stateful object. You can't reuse it. In particular, if you pass an Iterator to a function, you can't use it afterwards - you should create a new Iterator. It is not always possible though.
The easiest way to fix this code is to pass a collection to the function instead of an iterator:
pushAndLog(array, [...map.keys()]);
But this solution is generally not the most efficient, as creation of a new collection consumes memory and CPU time. There's a better solution. Add the following utilities to your code:
export function getIterableKeys<K, V>(map: Iterable<readonly [K, V]>): Iterable<K> {
return {
[Symbol.iterator]: function* () {
for (const [key, _] of map) {
yield key;
}
}
};
}
export function getIterableValues<K, V>(map: Iterable<readonly [K, V]>): Iterable<V> {
return {
[Symbol.iterator]: function* () {
for (const [_, value] of map) {
yield value;
}
}
};
}
Use them instead of native Map.keys() and Map.values() methods:
pushAndLog(array, getIterableKeys(map));
It works better for bigger maps.
Notes:
export
keywords.Returning an Iterator object from Map.keys() and Map.values() methods is a bad design decision we have to live with and always keep in mind. Such errors are sometimes unexpected and very hard to track, as iterators serve well until you try to reuse them. So you have to stay vigilant.
In other mature programming languages such methods smartly return iterable objects:
Both Java Iterable and .NET IEnumerable are reusable, so their users can't run into the same issue. This approach is much safer. It is a question for me why a relatively young JavaScript community invents their own solutions instead of inheriting best practices from existing mature programming languages.
Even though JavaScript Iterator works more or less like an Iterable object (you may use it in for..of loops, spread operators, collection constructors etc.), it formally breaks the Iterable class contract which is meant to be reusable, while Iterator is not.
The problem is amplified by TypeScript definitions which pretend that Map.keys() and Map.values() return a so-called IterableIterator object extending Iterable interface, which is wrong for the same reasons.