Understanding TypeScript generators
Tags: typescript, webdev
Published: 2023-06-29 15:13:30

**Written by [Debjyoti Banerjee](https://blog.logrocket.com/author/debjyotibanerjee/)✏️**

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:_

*   [Creating a generator function in TypeScript](#creating-generator-function-typescript)
*   [Understanding JavaScript iterables and iterators](#javascript-iterables-iterators)
*   [Working with generators in TypeScript](#working-with-generators)
*   [Use cases for TypeScript generators](#use-cases-typescript-generators)
    *   [Calculate values on demand](#calculate-values-on-demand)
    *   [Iterate over large data sets](#iterate-large-data-sets)
*   [Using generators recursively](#using-generators-recursively)
*   [Error handling](#error-handling)

## Creating a generator function in TypeScript <a name="creating-generator-function-typescript">

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 *`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) command. Generator functions look like normal functions, but they behave a little differently. Take a look at the following example:

```typescript
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: ![Calling Generator Fails Code Execution Example](http://blog.logrocket.com/wp-content/uploads/2023/06/calling-generator-fails-code-execution-example.png) 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:

```typescript
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:

```typescript
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.

## Understanding JavaScript iterables and iterators <a name="javascript-iterables-iterators">

Iterable objects are [objects that can be iterated over with `for..of`](https://blog.logrocket.com/javascript-iterators-and-generators-a-complete-guide/). They must implement the `Symbol.iterator` method; for example, [arrays in JavaScript are built-in iterables](https://blog.logrocket.com/javascript-array-methods/), so they must have an iterator:

```typescript
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:

```javascript
>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.

## Working with generators in TypeScript <a name="working-with-generators">

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:

```javascript
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:

```javascript
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:

```javascript
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
```

## Use cases for TypeScript generators <a name="use-cases-typescript-generators">

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.

### Calculate values on demand

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:

```javascript
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.

### Iterate over large data sets <a name="iterate-large-data-sets">

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:

```javascript
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.

## Using generators recursively

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:

```javascript
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:

```javascript
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:

```javascript
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`.

## Error handling

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`](https://blog.logrocket.com/improve-error-handling-typescript-exhaustive-type-checking/) block within the generator function or outside when consuming the generator:

```javascript
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:

```javascript
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.

## Conclusion

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.

---

##Get set up with LogRocket's modern Typescript error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via npm or script tag. `LogRocket.init()` must be called client-side, not server-side.

npm:
```javascript
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script tag:
```javascript
Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
```

3.(Optional) Install plugins for deeper integrations with your stack:
*   Redux middleware
*   ngrx middleware
*   Vuex plugin

[Get started now](https://lp.logrocket.com/blg/signup)