Iterators and generators are important patterns for handling collections in JavaScript/TypeScript.
They provide a unified way to traverse data, making it easier to handle large data streams, infinite sequences, and more.
Why We Need Iterators and Generators
When dealing with collection data, we often need to traverse data structures such as arrays and objects.
Iterators provide a unified, customizable traversal interface that allows any object to be iterated.
Generators are a concise way to create iterators, allowing you to use functions to pause and resume execution, which is very suitable for handling large data streams or infinite sequences.
Concept Explanation: An iterator is an object that provides a next() method for traversing data. A generator is a special kind of function that can pause during execution and return a value.
Iterable Protocol
Objects that implement the Symbol.iterator method can be traversed by for...of loops.
Example
// Arrays are iterable by default
var arr =[1,2,3];
for(var _i =0, arr_1 = arr; _i < arr_1.length; _i++){
var item = arr_1;
console.log("Array element: "+ item);
}
// Strings are iterable by default
var str ="hello";
for(var _i =0, str_1 = str; _i < str_1.length; _i++){
var char= str_1;
console.log("Character: "+char);
}
Output:
Array element: 1Array element: 2Array element: 3Character: h Character: e Character: l Character: l Character: o
Note: Arrays and strings both have built-in implementations of the Symbol.iterator method, so they can be directly traversed using for...of.
Custom Iterable Objects
Make ordinary objects implement the Symbol.iterator interface so they can be iterated.
Example
// Create a custom iterable object: range
var range ={
from:1,
to:5,
// Implement Symbol.iterator method
[Symbol.iterator]:function(){
return{
current:this.from,
last:this.to,
// next method returns iteration result
next:function(){
if(this.current<=this.last){
// Not done, return current value and increment
return{ done:false, value:this.current++};
}
// Done
return{ done:true, value:undefined};
}
};
}
};
// Use for...of to iterate
for(var _i =0, range_1 = range; _i < range_1.length; _i++){
var num = range_1;
console.log("Range: "+ num);
}
Iterator Protocol: An iterator must have a next() method that returns an object in the format { done: boolean, value: any }.
Generator Functions
Use the function* syntax to create generators, and use yield to pause execution and return a value.
Example
// Generator function: using function* syntax
function* numberGenerator(){
yield 1;// Pause and return 1
yield 2;// Pause and return 2
yield 3;// Pause and return 3
}
// Create generator instance
var gen = numberGenerator();
// Each call to next() executes until the next yield
console.log("First: "+ gen.next().value);
console.log("Second: "+ gen.next().value);
console.log("Third: "+ gen.next().value);
console.log("Done: "+ gen.next().done);
Output:
First: 1Second: 2Third: 3Done: true
Generator: A generator function returns an iterator. Each call to next() executes until the next yield statement.
Infinite Generator
Generators can produce infinite sequences. Due to lazy evaluation, they do not consume infinite memory.
Example
// Infinite number generator
// Each call only generates one number, not all numbers at once
function* infiniteNumbers(){
var n =1;
while (true){// Infinite loop
yield n++;// Pause and return current value, then increment
}
}
var gen = infiniteNumbers();
console.log("1st: "+ gen.next().value);
console.log("2nd: "+ gen.next().value);
console.log("3rd: "+ gen.next().value);
// Only get the first 5 numbers
var nums =[];
var iter = infiniteNumbers();
for(var i =0; i <5; i++){
nums.push(iter.next().value);
}
console.log("First 5: "+ nums);
Output:
1st: 12nd: 23rd: 3First 5: 1,2,3,4,5
Lazy Evaluation: The greatest advantage of generators is lazy evaluation. Values are only computed when next() is called, making them very suitable for handling infinite sequences.
Delegating Generators
Use yield* to delegate to another generator or iterable object.
Example
// First generator
function* gen1(){
yield 1;
yield 2;
}
// Second generator
function* gen2(){
yield 3;
yield 4;
}
// Combined generator: using yield* to delegate
function* combined(){
yield* gen1();// Delegate to gen1
yield* gen2();// Delegate to gen2
}
// Iterate over combined generator
for(var _i =0, combined_1 = combined(); _i < combined_1.length; _i++){
var num = combined_1;
console.log("Value: "+ num);
}
Output:
Value: 1Value: 2Value: 3Value: 4
yield*: Delegating generators can combine multiple generators or iterable objects, which is very suitable for building reusable data streams.
TypeScript Generator Types
Generator type annotations use the Generator type.
Example
// Generator type: Generator<yield type, return type, next parameter type>
function* idGenerator(): Generator<number,void, unknown>{
var i =1;
while (i <=3){
yield i++;// yield number type
}
// return void
}
var gen = idGenerator();
console.log(Array.from(gen));
Type Explanation: Generator<T, R, N> means: T is the yield type, R is the final return type, and N is the type of the next() parameter.
Notes
- Iterator Protocol: Implement Symbol.iterator to return an object with a next() method
- Generator Syntax: Use function* instead of function
- yield Keyword: Pauses execution and returns a value
- Lazy Evaluation: Generators compute on demand and do not generate all values at once
Best Practice: Use generators when handling large data streams, infinite sequences, or scenarios that require pause/resume.
Summary
Iterators and generators are powerful data processing tools in TypeScript.
- Iterable Objects: Implement the Symbol.iterator interface
- Generators: Created using function* and yield
- yield: Pauses execution and returns a value
- Delegation: Use yield* to combine multiple generators
Recommendation: Use iterators and generators when you need to traverse custom objects, handle data streams, or create infinite sequences.
YouTip