Iterators and Generators in TypeScript: A Complete Guide

Updated: January 8, 2024 By: Guest Contributor Post a comment

Overview

This guide dives into the intricacies of iterators and generators in TypeScript, exploring their syntax, use-cases, and benefits, complete with adaptable code examples to engrain best practices and advanced utilization strategies.

Introduction to Iterators

An iterator is an object that enables us to traverse through all the elements of a collection, regardless of its specific data structure. In TypeScript, they are a crucial part of iterable objects like Arrays, Maps, and Sets. Let us begin with a fundamental example of an iterator in an array.

let numbers = [1, 2, 3];
let iterator = numbers[Symbol.iterator]();
console.log(iterator.next().value); // outputs: 1
console.log(iterator.next().value); // outputs: 2
console.log(iterator.next().value); // outputs: 3

Now, we will implement a custom iterable object using the iterator protocols.

class CustomIterable { 
    items: Array<any>;
    constructor(items: Array<any>) {
        this.items = items;
    }
    [Symbol.iterator]() { 
        let i = 0;
        let items = this.items;
        return { 
            next() { 
                if(i < items.length) {
                    return { value: items[i++], done: false };
                } else { return { done: true };
                }
            }
        };
    }
}
let myIterable = new CustomIterable([...'abc']);
for(let item of myIterable) { 
    console.log(item);
}

As you can see, creating a custom iterator complements the native “for-of” loop and enables us to efficiently traverse through our CustomIterable’s elements.

Understanding Generators

A generator in TypeScript is a special kind of function that can be paused and resumed, making it very powerful for creating complex iterators with less effort. We define a generator with the function* notation and use yield to emit values.

function* simpleGenerator() {
    yield 'Hello';
    yield 'World';
}
const generatorObject = simpleGenerator();
console.log(generatorObject.next().value); // outputs: Hello
console.log(generatorObject.next().value); // outputs: World
console.log(generatorObject.next().done); // outputs: true

We can leverage the capabilities of generators to create easy-to-read asynchronous flows using TypeScript’s async/await syntax. The following example demonstrates how we can synchronize asynchronous code using a generator function.

function delay(milliseconds: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, milliseconds));
}

function* asyncFlow() {
    yield delay(1000);
    console.log('Print after 1 second');
    yield delay(2000);
    console.log('Print after 2 seconds');
}

(async () => { 
    const asyncGenerator = asyncFlow();
    await asyncGenerator.next().value;
    await asyncGenerator.next().value;
})( );

Note how we wait for each yield with an asynchronous action before moving to the next step.

Advanced Practices

Advanced usage of iterators in TypeScript includes designing complex data structures. Here is an example of how to build a binary tree’s iterator.

class BinaryTree<T> {
    value: T;
    left: BinaryTree<T> | null;
    right: BinaryTree<T> | null;
    constructor(value: T) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
    *inOrder(): IterableIterator<T> {
        if (this.left) {
            yield* this.left.inOrder();
        }
        yield this.value;
        if (this.right) {
            yield* this.right.inOrder();
        }
    }
}

let tree = new BinaryTree(1);
tree.left = new BinaryTree(2);
tree.right = new BinaryTree(3);

for(let value of tree.inOrder()) {
    console.log(value);
}

This example shows a rudimentary binary tree with an in-order traversal generator, illustrating the seamless integration of iterators and complex data structures.

Another advanced topic is the custom implementation of asynchronous iterators with generators. Asynchronous iterators are useful for consuming data streams asynchronously, such as handling events or parsing large files.

async function* asyncNumberGenerator(start: number, end: number) {
    for(let i = start; i <= end; i++) {
        await delay(500); // assume delay is defined as before
        yield i;
    }
}

(async () => {
    for await (const num of asyncNumberGenerator(1, 5)) {
        console.log(num);
    }
})(); 

The for-await-of loop, combined with an asynchronous generator, simplifies the iteration over async operations with a yet familiar syntax.

Final Words

Iterators and generators are potent tools in a TypeScript developer’s arsenal, providing elegant solutions for data traversal and control flow handling. By understanding and effectively leveraging these concepts, crafting both synchronously and asynchronously iterable structures can become a more intuitive and efficient process.