Oct 20, 2024

A Deep Dive into Generators in TypeScript

What Are Generators?

Generators are special functions in JavaScript (and TypeScript) that can pause their execution and later resume it, returning multiple values over time. Unlike regular functions that return a single value, Generators allow you to yield a value at any point during execution.

Here’s the basic syntax:

1function* generatorFunction() {
2 yield 1;
3 yield 2;
4 yield 3;
5}

Generators are marked by the * after the function keyword, and inside the function body, you use the yield keyword to produce a value. Calling a generator function doesn’t execute it immediately but returns an iterator object, which you can control using methods like next().

How Generators Work

Generators work through the Iterator protocol. Each time you call the next() method on a generator, it runs the function up until the next yield statement. At that point, the generator’s execution is paused, and the yielded value is returned. When next() is called again, execution resumes from the point it left off.

Here’s a simple example:

1function* simpleGenerator() {
2 console.log('Execution started');
3 yield 1;
4 console.log('Yielded 1');
5 yield 2;
6 console.log('Yielded 2');
7 return 3; // After the last yield, the generator completes
8}
9
10const generator = simpleGenerator();
11
12console.log(generator.next()); // { value: 1, done: false }
13console.log(generator.next()); // { value: 2, done: false }
14console.log(generator.next()); // { value: 3, done: true }

In this example, the generator pauses at each yield and logs the flow of execution to the console. The final return marks the end of the generator, and it signifies the done: true state.

Strong Typing with TypeScript

Generators in TypeScript allow you to strongly type both the values that are yielded and the return value of the generator function. The syntax for typing a generator is:

1function* generatorFunction(): Generator<YieldType, ReturnType, NextType> {
2// generator logic
3}

For example, here’s a generator that yields numbers and returns a string:

1function* numberGenerator(): Generator<number, string, void> {
2yield 1;
3yield 2;
4yield 3;
5return "Finished!";
6}
7
8const gen = numberGenerator();
9
10console.log(gen.next()); // { value: 1, done: false }
11console.log(gen.next()); // { value: 2, done: false }
12console.log(gen.next()); // { value: 3, done: false }
13console.log(gen.next()); // { value: 'Finished!', done: true }

Here, numberGenerator() is typed to yield number values and return a string when the generator finishes.

Advanced Usage: Passing Values into Generators

One of the powerful aspects of generators is that you can pass values back into them during execution. This can be useful when building workflows where each iteration depends on external input.

Let’s take an example where we calculate running totals based on external input:

1function* runningTotalGenerator(): Generator<number, void, number> {
2let total = 0;
3let increment = yield total;
4
5while (true) {
6total += increment;
7increment = yield total;
8}
9}
10
11const totalGen = runningTotalGenerator();
12console.log(totalGen.next()); // { value: 0, done: false }
13console.log(totalGen.next(5)); // { value: 5, done: false }
14console.log(totalGen.next(10)); // { value: 15, done: false }
15console.log(totalGen.next(20)); // { value: 35, done: false }

In this example:

Generator Delegation with yield*

You can also delegate control from one generator to another using the yield* expression. This allows you to compose generators, which can be very useful when breaking down complex tasks into smaller, reusable parts.

Here’s a simple example:

1function* subGenerator() {
2yield 'a';
3yield 'b';
4}
5
6function* mainGenerator() {
7yield 1;
8yield* subGenerator(); // Delegates to subGenerator
9yield 3;
10}
11
12const gen = mainGenerator();
13
14console.log(gen.next()); // { value: 1, done: false }
15console.log(gen.next()); // { value: 'a', done: false }
16console.log(gen.next()); // { value: 'b', done: false }
17console.log(gen.next()); // { value: 3, done: false }

In this example, the mainGenerator() delegates its control to subGenerator(). When subGenerator() completes, mainGenerator() resumes its execution.

Asynchronous Generators

Starting with ECMAScript 2018, JavaScript introduced Asynchronous Generators. These are generators that work with Promises, allowing you to pause execution and resume when an asynchronous operation is complete.

You can define an async generator using the async and * keywords together. Here’s an example where we fetch data asynchronously in a loop:

1async function* fetchDataGenerator(urls: string[]): AsyncGenerator<any, void, unknown> {
2for (const url of urls) {
3 try {
4 const response = await fetch(url);
5 if (!response.ok) {
6 throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
7 }
8 const data = await response.json();
9 yield data;
10 } catch (error) {
11 console.error(`Error fetching data from ${url}: `, error);
12 yield { error: true, message: error.message }; // Optionally yield an error object
13 }
14}
15}
16
17(async () => {
18const urls = ["https://api.example.com/data1", "https://api.example.com/data2"];
19const dataGen = fetchDataGenerator(urls);
20
21for await (const data of dataGen) {
22if (data.error) {
23console.error(`Error: ${data.message}`);
24} else {
25console.log(data); // Logs valid data from each URL
26}
27}
28})();

In this example, the fetchDataGenerator asynchronously fetches data from a list of URLs and yields the results one by one.

Real-World Use Cases of Generators
  1. Lazy Evaluation: Generators are perfect for scenarios where you want to evaluate values lazily. Instead of creating large arrays or datasets up front, you can generate them one by one as needed, saving memory.
  2. Asynchronous Control Flow: Generators can be used to simplify asynchronous code, especially when combined with libraries like co or async. They provide a structured way to write code that looks synchronous but handles asynchronous logic under the hood.
  3. Iterating over Large Data Streams: For scenarios like parsing large files, generators allow you to process chunks of data without loading the entire file into memory at once.
Conclusion

Generators in TypeScript offer a powerful way to write more flexible and efficient code. Whether you are working with synchronous or asynchronous workflows, Generators allow you to handle complex logic with ease, yielding values over time. When combined with TypeScript’s strong typing, they become even more robust.

In this guide, we covered the basics of using Generators, passing values into Generators, delegation with yield*, and the use of async Generators. Armed with this knowledge, you can start applying Generators to your own projects, simplifying both synchronous and asynchronous logic.

contact

padhysoumya98@gmail.com

Soumya

2025