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()
.
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 completes8}910const generator = simpleGenerator();1112console.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.
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 logic3}
YieldType
: The type of values the generator will yield
.ReturnType
: The type of value the generator will return
when completed.NextType
: The type of value that can be passed back into the generator through next()
.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}78const gen = numberGenerator();910console.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.
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;45while (true) {6total += increment;7increment = yield total;8}9}1011const 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:
0
.next(5)
is called, 5
is passed into the generator, and it becomes the first increment.next()
keep adding to the total based on the passed values.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}56function* mainGenerator() {7yield 1;8yield* subGenerator(); // Delegates to subGenerator9yield 3;10}1112const gen = mainGenerator();1314console.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.
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 object13 }14}15}1617(async () => {18const urls = ["https://api.example.com/data1", "https://api.example.com/data2"];19const dataGen = fetchDataGenerator(urls);2021for await (const data of dataGen) {22if (data.error) {23console.error(`Error: ${data.message}`);24} else {25console.log(data); // Logs valid data from each URL26}27}28})();
In this example, the fetchDataGenerator
asynchronously fetches data from a list of URLs and yields the results one by one.
co
or async
. They provide a structured way to write code that looks synchronous but handles asynchronous logic under the hood.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.