Did you ever encounter a situation where a method or a library you were using returned an iterator (as typed in using TypeScript: IterableIterator
)? Are you like me, early in my career, and tried to loop over iterators using .map()
, .forEach()
, .filter()
or any other array method? Not being aware that this is not possible, all of a sudden you get a TypeError: iterator.map is not a function
or a Property 'map' does not exist on type 'IterableIterator<string>'.
thrown at you. And now you're thinking: "Why? Why can't I iterate over an iterator? What's going on?"
Let's find out.
What is an iterator?
According to mdn web docs: "In JavaScript an iterator is an object which defines a sequence and potentially a return value upon its termination. Specifically, an iterator is any object which implements the Iterator protocol by having a next() method that returns an object with two properties:"
The most important part of this description is that an iterator iterates over an object. Now try to picture what you're iterating over when you iterate over an object. Do you iterate over the keys, over the values, over values of values? That is exactly what you can establish by implementing the Iterator protocol into an object. Once an object has an Iterator protocol implemented the iterable protocol allows JavaScript objects to define or customize their iteration behavior, such as what values are looped over in a for...of
statement.
Implement the Iterator protocol into an object
To implement the Iterator protocol into an object we first need an object:
const people = {
0: {
firstName: "Niek",
lastName: "Nijland",
age: 30,
},
1: {
firstName: "Tim",
lastName: "Cook",
age: 63,
},
};
To check if this object has an Iterator protocol implemented we can try to iterate over it using a for...of
loop.
for (const person of people) {
console.log(person);
}
// TypeError: people is not iterable
This results in a TypeError
saying that people
is not iterable.
Let's fix that by implementing the iterator function.
The iterator function
An iterator function should return a method named next()
that on its turn returns an object with two properties:
- value -> The value in this iteration sequence
- done -> A boolean that that should be
false
when the iteration is not finished yet andtrue
when the last value of the iteration is consumed.
In our case we want the individual person
objects to be returned as value
.
That translates to something like this:
function iterator() {
let index = 0;
return {
next: () => {
if (index > people.length - 1) {
return {
done: true,
value: undefined,
};
}
const person = this[index];
index++;
return {
done: false,
value: person,
};
},
};
}
This raises two questions:
- How does
for...of
know that it should execute this function? - Where does
people.length
come from? people is not an array after all..
To answer the first question: When JavaScript encounters a for...of
loop it searches for the iterator function in the object's Symbol.iterator
property. Let's add our iterator function to our people
object.
const people = {
// ...
[Symbol.iterator]: function () {
let index = 0;
return {
next: () => {
if (index > people.length - 1) {
return {
done: true,
value: undefined,
};
}
const person = this[index];
index++;
return {
done: false,
value: person,
};
},
};
},
};
Running it now results in:
for (const person of people) {
console.log(person);
}
// Output
// { firstName: 'Niek', lastName: 'Nijland', age: 30 }
// { firstName: 'Tim', lastName: 'Cook', age: 63 }
// undefined
// undefined
// undefined
// etc
An infinite loop!
This is where we answer the second question. people.length
does not exist! That results in the done
property never being set to true
. Let's add it to our people
object to finish the implementation of the Iterator protocol.
const people = {
0: {
firstName: "Niek",
lastName: "Nijland",
age: 30,
},
1: {
firstName: "Tim",
lastName: "Cook",
age: 63,
},
length: 2,
[Symbol.iterator]: function () {
let index = 0;
return {
next: () => {
if (index > people.length - 1) {
return {
done: true,
value: undefined,
};
}
const person = this[index];
index++;
return {
done: false,
value: person,
};
},
};
},
};
You could of course stop the iteration in numerous other ways. For example, by retrieving the length from Object.keys(people)
. How and when to stop the iteration is completely dependent on the implementation.
In this example we created a simplified implementation of the Array
object. Does it ring a bell? How would you access an item in an array with index 1? Right: people[1]
. How would you get the length of an array? people.length
. Array's are objects in JavaScript! That's why we call it the Array object.
Running the for...of
loop over the people
object one last time results in:
for (const person of people) {
console.log(person);
}
// Output:
// { firstName: 'Niek', lastName: 'Nijland', age: 30 }
// { firstName: 'Tim', lastName: 'Cook', age: 63 }
How does it work?
Let's say the for...of
loop does not exist or you cannot use it for some reason. How do you iterate over the people
object. Or phrased differently: What does for...of
do to iterate over the people
object?
First it initiates the iterator:
const iterator = people[Symbol.iterator]();
console.log(iterator);
// { next: [Function: next] }
Now it has the iterator as a constant. The next()
method can be called to move to the next sequence in the iteration.
const iterator = people[Symbol.iterator]();
console.log(iterator.next());
/**
* {
* done: false,
* value: { firstName: 'Niek', lastName: 'Nijland', age: 30 }
* }
*/
As you can see this returns the iterator result object. The value can be accessed with .value
and checking if the iteration is done by checking if done
is true
.
In this case done
is false
. That means next()
can be called one more time.
// ...
console.log(iterator.next());
/**
* {
* done: false,
* value: { firstName: 'Tim', lastName: 'Cook', age: 63 }
* }
*/
Let's do it one more time to finish the iteration.
// ...
console.log(iterator.next());
/**
* {
* done: true,
* value: undefined
* }
*/
This is a bare bones representation of what happens when a for...of
loop is called on an Iterator object.
Back to the intro example.. What was going on?
Why couldn't we loop over the iterator using array methods? Now that we now that an array is an object and we know what the structure is of such an object we can answer that question.
Let's say the iterator you were trying to loop over was our people
object. We did not implement map
, forEach
or any other methods that an Array object has implemented. Trying to access people.map
will result in TypeError: iterator.map is not a function
since we did not implement that method.
You can use these methods on an Array object. Array
is a type that is built-in into browsers. It has a built-in iterator protocol and built-in methods (e.g. map
, forEach
, etc). There are a lot more built-in types that already have default iteration behavior, like Map
and Set
. Now you know how it is possible to iterate over these types!
To give an example of a subset of browser API's that will provide you with an iterator you can check out the TypeScript source code.
Conclusion
I hope you learned the basics of iterators from this blog post. There's a lot more to iterators then the simple examples in this blog post. The next()
method could optionally throw
instead of return
an iterator result for example or it could be implemented asynchronous using the async iterator and async iterable protocols. Those topics exceed the intentions of this blog post. I invite you to read these two MDN pages to learn more about it:
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
If you liked this article and want to read more make sure to check the my other articles. Feel free to contact me on Twitter with tips, feedback or questions!
Top comments (0)