Normal functions run from top to bottom and then exit. Generator functions also run from top to bottom, but they can be paused during execution and resumed later from the same point. This goes on until the end of the process, and then they exit. In this article, we’ll learn how to use generator functions in TypeScript, covering a few different examples and use cases. Let’s get started!
Jump ahead:
Normal functions are eager, whereas generators are lazy, meaning they can be asked to execute at a later point in time. To create a generator function, we‘ll use the function *
command. Generator functions look like normal functions, but they behave a little differently. Take a look at the following example:
function normalFunction() { console.log("This is a normal function"); } function* generatorFunction() { console.log("This is a generator function"); } normalFunction(); // "This is a normal function" generatorFunction();
Although it is written and executed just like a normal function, when generatorFunction
is called, we don’t get any logs in the console. Put simply, calling the generator won’t execute the code:
You’ll notice that the generator function returns a Generator
type; we’ll look at this in detail in the next section. To make the generator execute our code, we‘ll do the following:
function* generatorFunction() { console.log("This is a generator function"); } const a = generatorFunction(); a.next();
Notice that the next
method returns an IteratorResult
. So, if we were to return a number from generatorFunction
, we would access the value as follows:
function* generatorFunction() { console.log("This is a generator function"); return 3; } const a = generatorFunction(); const b = a.next(); console.log(b); // {"value": 3, "done": true} console.log(b.value); // 3
The generator interface extends Iterator
, which allows us to call next
. It also has the [Symbol.iterator]
property, making it an iterable.
Iterable objects are objects that can be iterated over with for..of
. They must implement the Symbol.iterator
method; for example, arrays in JavaScript are built-in iterables, so they must have an iterator:
const a = [1,2,3,4]; const it: Iterator<number> = a[Symbol.iterator](); while (true) { let next = it.next() if (!next.done) { console.log(next.value) } else { break; } }
The iterator makes it possible to iterate the iterable. Take a look at the following code, which is a very simple implementation of an iterator:
function naturalNumbers() { let n = 0; return { next: function() { n += 1; return {value:n, done:false}; } }; } const iterable = naturalNumbers(); iterable.next().value; // 1 iterable.next().value; // 2 iterable.next().value; // 3 iterable.next().value; // 4
As mentioned above, an iterable is an object that has the Symbol.iterator
property. So, if we were to assign a function that returns the next()
function, like in the example above, our object would become a JavaScript iterable. We could then iterate over it using the for..of
syntax.
Obviously, there is a similarity between the generator function we saw earlier and the example above. In fact, since generators compute one value at a time, we can easily use generators to implement iterators.
The exciting thing about generators is that you can pause execution using the yield
statement, which we didn’t do in our previous example. When next
is called, the generator executes code synchronously until a yield
is encountered, at which point it pauses the execution. If next
is called again, it will resume execution from where it was paused. Let’s look at an example:
function* iterator() { yield 1 yield 2 yield 3 } for(let x of iterator()) { console.log(x) }
yield
basically allows us to return multiple times from the function. In addition, an array will never be created in memory, allowing us to create infinite sequences in a very memory efficient manner. The following example will generate infinite even numbers:
function* evenNumbers() { let n = 0; while(true) { yield n += 2; } } const gen = evenNumbers(); console.log(gen.next().value); //2 console.log(gen.next().value); //4 console.log(gen.next().value); //6 console.log(gen.next().value); //8 console.log(gen.next().value); //10
We can also modify the example above so that it takes a parameter and yields even numbers, starting from the number provided:
function* evenNumbers(start: number) { let n = start; while(true) { if (start === 0) { yield n += 2; } else { yield n; n += 2; } } } const gen = evenNumbers(6); console.log(gen.next().value); //6 console.log(gen.next().value); //8 console.log(gen.next().value); //10 console.log(gen.next().value); //12 console.log(gen.next().value); //14
Generators provide a powerful mechanism for controlling the flow of data and creating flexible, efficient, and readable code in TypeScript. Their ability to produce values on-demand, handle asynchronous operations, and create custom iteration logic makes them a valuable tool in a few scenarios.
You can implement generators to calculate and yield values on-demand, caching intermediate results to improve performance. This technique is useful when dealing with expensive computations or delaying the execution of certain operations until they are actually needed. Let’s consider the following example:
function* calculateFibonacci(): Generator<number> { let prev = 0; let curr = 1; yield prev; yield curr; while (true) { const next = prev + curr; yield next; prev = curr; curr = next; } } // Using the generator to calculate Fibonacci numbers lazily const fibonacciGenerator = calculateFibonacci(); // Calculate the first 10 Fibonacci numbers for (let i = 0; i < 10; i++) { console.log(fibonacciGenerator.next().value); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 }
In the example above, instead of computing all Fibonacci numbers up front, only the required Fibonacci numbers are calculated and yielded as they are requested. This results in more efficient memory usage and on-demand calculation of values as needed.
Generators allow you to iterate over large data sets without loading all the data into memory at once. Instead, you can generate values as needed, thereby improving memory efficiency. This is particularly useful when working with large databases or files:
function* iterateLargeData(): Generator<number> { const data = Array.from({ length: 1000000 }, (_, index) => index + 1); for (const item of data) { yield item; } } // Using the generator to iterate over the large data set const dataGenerator = iterateLargeData(); for (const item of dataGenerator) { console.log(item); // Perform operations on each item without loading all data into memory }
In this example, the iterateLargeData
generator function simulates a large data set by creating an array of one million numbers. Instead of returning the entire array at once, the generator yields each item one at a time using the yield
keyword. Therefore, you can iterate over the data set without loading all the numbers into memory simultaneously.
The memory efficient properties of generators can be put to use for something more useful, like reading file names inside a directory recursively. In fact, recursively traversing nested structures is what comes naturally to me when thinking about generators.
Since yield
is an expression, yield*
can be used to delegate to another iterable object, as shown in the following example:
function* readFilesRecursive(dir: string): Generator<string> { const files = fs.readdirSync(dir, { withFileTypes: true }); for (const file of files) { if (file.isDirectory()) { yield* readFilesRecursive(path.join(dir, file.name)); } else { yield path.join(dir, file.name); } } }
We can use our function as follows:
for (const file of readFilesRecursive('/path/to/directory')) { console.log(file); }
We can also use yield
to pass a value to the generator. Take a look at the following example:
function* sumNaturalNumbers(): Generator<number, any, number> { let value = 1; while(true) { const input = yield value; value += input; } } const it = sumNaturalNumbers(); it.next(); console.log(it.next(2).value); //3 console.log(it.next(3).value); //6 console.log(it.next(4).value); //10 console.log(it.next(5).value); //15
When next(2)
is called, input
is assigned the value 2
; similarly, when next(3)
is called, input is assigned the value 3
.
Exception handling and controlling the flow of execution is an important concept to discuss if you want to work with generators. Generators basically look like normal functions, so the syntax is the same.
When a generator encounters an error, it can throw an exception using the throw
keyword. This exception can be caught and handled using a try...catch
block within the generator function or outside when consuming the generator:
function* generateValues(): Generator<number, void, string> { try { yield 1; yield 2; throw new Error('Something went wrong'); yield 3; // This won't be reached } catch (error) { console.log("Error caught"); yield* handleError(error); // Handle the error and continue } } function* handleError(error: Error): Generator<number, void, string> { yield 0; // Continue with a default value yield* generateFallbackValues(); // Yield fallback values throw `Error handled: ${error.message}`; // Throw a new error or rethrow the existing one } const generator = generateValues(); console.log(generator.next()); // { value: 1, done: false } console.log(generator.next()); // { value: 2, done: false } console.log(generator.next()); // Error caught // { value: 0, done: false } console.log(generator.next()); // { value: 4, done: false } console.log(generator.next()); // Error handled: Something went wrong
In this example, the generateValues
generator function throws an error after yielding the value 2
. The catch block within the generator catches the error, and the control is transferred to the handleError
generator function, which yields fallback values. Finally, the handleError
function throws a new error or re-throws the existing one.
When consuming the generator, you can catch the thrown errors using a try...catch
block as well:
const generator = generateValues(); try { console.log(generator.next()); console.log(generator.next()); console.log(generator.next()); } catch (error) { console.error('Caught error:', error); }
In this case, the error will be caught by the catch block, and you can handle it accordingly.
In this article, we learned how to use generators in TypeScript, reviewing their syntax and foundation in JavaScript iterators and iterables. We also learned how to use TypeScript generators recursively and handle errors using generators.
You can use generators for a lot of interesting purposes, like generating unique IDs, generating prime numbers, or implementing stream-based algorithms. You can control the termination of the sequence using a condition or by manually breaking out of the generator. I hope you enjoyed this article, and be sure to leave a comment if you have any questions.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
5 Replies to "Understanding TypeScript generators"
An obscure feature of a language that has much bigger issues to deal with first. This is probably the fifth article I read about it, but I’ve never seen anyone actually use it in the wild, including me.
I agree. And it’s gimmicky.
As an article, it’s good; but as a dev pattern I could imagine one of my junior, programmers burning up hours—or even days or weeks—looking for clever ways to use this feature (a “clever” solution looking for the ideal problem) …only to have the next maintainer come along and rip it out for something more straightforward.
I think this next() call:
console.log(generator.next()); // Uncaught Error: Something went wrong
Is not correct. That error doesn’t get thrown out of the generator because it is caught within the generator.
Generators are a JavaScript language feature, not a Typescript feature. How can we take the author seriously if he doesn’t even know the most fundamental fact about the topic he’s discussing
I think you need to chill out. It IS technically a feature in typescript too, as TS supports whatever JS supports, but really, does an author have to state the obvious for it to be taken seriously? Maybe people learn TS and not JS, or, maybe people don’t care.