TypeScript Generator
Introduced in ES2015, Generators represent a significant evolution in JavaScript's control flow. Unlike standard functions that run to completion, a generator can pause its execution mid-way, yield a value, and later resume exactly where it left off. This capability enables powerful patterns such as lazy evaluation, custom iteration logic, and efficient handling of large data streams.
While the runtime behavior of generators is a core JavaScript feature, TypeScript elevates them by providing a robust type system. By leveraging TypeScript's generics, developers can ensure that the values yielded, the values returned upon completion, and the values passed back into the generator via .next() are all strictly typed, preventing common runtime errors in complex iteration logic.
The Mechanics of Generator Functions
A generator function is defined using the function* syntax. When called, it does not execute its body immediately; instead, it returns a Generator Object. This object conforms to both the Iterator and Iterable protocols, meaning it can be used in for...of loops, spread operators, and manual iteration.
The lifecycle of a generator is managed through three primary methods:
next(value): Resumes the generator's execution. Thevalueprovided as an argument becomes the result of the currentyieldexpression within the function.return(value): Forces the generator to terminate immediately, returning the specified value.throw(error): Injects an error into the generator at its current suspension point, allowing for internal error handling viatry...catchblocks.
Consider this fundamental implementation:
function* range(start: number, end: number): Generator<number, void, unknown> {
for (let i = start; i < end; i++) {
yield i;
}
}
const it = range(1, 4);
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log([...it]); // [3] -> Note: the spread operator consumes the remaining values
Mastering the Generator<T, TReturn, TNext> Type
The true strength of using generators in a TypeScript environment lies in the Generator generic type. To achieve full type safety, you must understand its three parameters:
T(Yield Type): The type of values produced by theyieldkeyword.TReturn(Return Type): The type of the value returned when the generator reaches areturnstatement or finishes its execution.TNext(Next Type): The type of the value passed into the.next(value)method. This is also the type that theyieldexpression evaluates to inside the function.
A common point of confusion is the relationship between TNext and the yield expression. When you call gen.next(val), that val becomes the result of the yield statement inside the generator.
Here is a sophisticated example demonstrating all three types:
function* idGenerator(): Generator<number, string, boolean> {
let id = 1;
while (true) {
// The value yielded is a number (T)
// The value received from next() is a boolean (TNext)
const keepGoing = yield id++;
if (!keepGoing) {
return "stopped"; // The final return value is a string (TReturn)
}
}
}
const gen = idGenerator();
// First call: we pass nothing, so the first 'yield' receives 'undefined'
// However, TNext is boolean, so we should be mindful of the first call.
console.log(gen.next()); // { value: 1, done: false }
// Passing 'true' into the generator
console.log(gen.next(true)); // { value: 2, done: false }
// Passing 'false' to trigger the return statement
console.log(gen.next(false)); // { value: "stopped", done: true }
Pro-tip: The very first call to .next() cannot accept an argument to be used by a yield expression (as no yield has been reached yet). If your TNext is not void, ensure your logic accounts for the initial undefined or design the type to be compatible with the first call.
If your generator only produces values and does not require external input, you can simplify the signature using void or unknown:
function* simpleNumbers(): Generator<number, void, unknown> {
yield 1;
yield 2;
}
Composition via yield*
Generators are highly composable. The yield* expression allows a generator to delegate its execution to another iterable object or another generator. This is particularly useful for flattening nested structures or combining multiple data sources into a single stream.
function* flatten<T>(iterables: Iterable<T>[]): Generator<T, void, unknown> {
for (const iterable of iterables) {
// Delegate to the current iterable
yield* iterable;
}
}
const nested = [[1, 2], [3, 4], [5]];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5]
Using yield* is more than just syntactic sugar; it ensures that the delegation respects the return and throw semantics of the sub-generator, maintaining a clean and predictable execution flow.
Asynchronous Generators: Handling Streams
In modern web development, we often deal with asynchronous data, such as paginated API responses or real-time WebSocket streams. For these scenarios, TypeScript provides AsyncGenerator<T, TReturn, TNext>.
An async function* allows you to use await inside the generator body, making it possible to pause execution while waiting for a Promise to resolve.
async function* fetchPages(urls: string[]): AsyncGenerator<string, void, unknown> {
for (const url of urls) {
const response = await fetch(url);
const text = await response.text();
yield text;
}
}
async function consumeStream() {
// Use 'for await...of' to consume an AsyncGenerator
for await (const content of fetchPages(["/api/v1", "/api/v2"])) {
console.log("Received content:", content);
}
}
To consume an AsyncGenerator, you must use the for await...of loop. Note that this requires your compilation target to be at least ES2018.
Compilation and Configuration Requirements
To use generators effectively, your tsconfig.json must be configured correctly to match your target environment.
{
"compilerOptions": {
"target": "ES2018",
"lib": ["ES2018", "DOM"],
"downlevelIteration": true
}
}
Key considerations:
target: If you set your target toES2015or higher, generators are supported natively.downlevelIteration: If you are targeting older environments likeES5, TypeScript will use a state-machine approach to emulate generators. EnablingdownlevelIterationis crucial for ensuring that iteration protocols (likefor...of) work correctly in these downgraded environments. This may require thetslibhelper library.lib: Ensure your library declarations include the necessary definitions for generators and async iterables (e.g.,es2015.generator,es2018.asynciterable).
Summary
TypeScript Generators bridge the gap between high-level, expressive iteration patterns and strict, type-safe development. By mastering the Generator<T, TReturn, TNext> generic structure, utilizing yield* for composition, and leveraging AsyncGenerator for asynchronous workflows, you can build highly efficient, readable, and robust data processing pipelines.