Skip to Content
Course content

205: Typing Generator Functions

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Generators are one of those features that often sit in the "I know they exist, but I'm not sure when to actually use them" category. But once you start dealing with large datasets or complex state machines, they're a lifesaver. The tricky part is that TypeScript's Generator type takes three generic arguments, and if you've never seen them, it looks like a bit of a riddle.

Let's build a batchProcessor. Imagine we have a massive list of user IDs that we need to process in chunks to avoid hitting API rate limits. This is a perfect spot for a generator because we don't want to slice the whole array into memory upfront; we want to yield chunks on demand.

Drafting the basic batcher

I'll start by sketching out the logic. I want to pass in an array and a batch size, and then yield a slice of that array every time .next() is called.

function* batchProcessor<T>(items: T[], batchSize: number) {
  for (let i = 0; i < items.length; i += batchSize) {
    yield items.slice(i, i + batchSize);
  }
}

TypeScript is actually pretty smart here. If I don't provide an explicit return type, it infers it. But in a professional codebase, we shouldn't rely on inference for the public API of a utility function. We need to be explicit about what this generator produces, what it returns when it's finished, and what it accepts if we send values back into it.

Defining the Generator generic

The Generator type looks like this: Generator<T, TReturn, TNext>. Here is how I break that down in my head:

  • T: What the generator yields (the value you get from .next().value).
  • TReturn: What the generator returns when it finally finishes (the final .next().value when done is true).
  • TNext: What the generator receives when you call .next(value).

For our batcher, we are yielding arrays of T, we aren't returning anything special at the end, and we aren't sending any values back in. So, it should look like this:

function* batchProcessor<T>(items: T[], batchSize: number): Generator<T[], void, unknown> {
  for (let i = 0; i < items.length; i += batchSize) {
    yield items.slice(i, i + batchSize);
  }
}

Correcting a common return type slip

Here is where I usually make a mistake when I'm rushing. I'll try to return the total number of batches processed at the end of the function to be "helpful," but I'll forget to update the TReturn type.

// This will throw a TypeScript error!
function* batchProcessor<T>(items: T[], batchSize: number): Generator<T[], void, unknown> {
  let count = 0;
  for (let i = 0; i < items.length; i += batchSize) {
    count++;
    yield items.slice(i, i + batchSize);
  }
  return count; // Error: Type 'number' is not assignable to type 'void'.
}

I just tried to return a number, but I told TypeScript the return type was void. To fix this, I need to change that second generic argument from void to number. It's a small change, but it's the difference between your code compiling and a frustrating morning of debugging.

function* batchProcessor<T>(items: T[], batchSize: number): Generator<T[], number, unknown> {
  let count = 0;
  for (let i = 0; i < items.length; i += batchSize) {
    count++;
    yield items.slice(i, i + batchSize);
  }
  return count;
}

Handling the TNext argument

Now, let's get fancy. What if we want to be able to "skip" a batch by sending a signal back into the generator? We can use the TNext parameter. Instead of unknown, let's say we want to send a boolean to indicate if we should double the batch size for the next iteration.

function* batchProcessor<T>(
  items: T[], 
  batchSize: number
): Generator<T[], number, boolean> {
  let count = 0;
  let currentSize = batchSize;
  
  for (let i = 0; i < items.length; i += currentSize) {
    count++;
    // The value passed into .next() becomes the result of the yield expression
    const shouldDouble = yield items.slice(i, i + currentSize);
    
    if (shouldDouble) {
      currentSize *= 2;
    }
  }
  return count;
}

Now the generator is fully typed. It yields T[], returns a number, and accepts a boolean. This is a powerful pattern for creating bidirectional communication between the caller and the iterator.




📋 Practical Task

Exercise: Build a Paginated API Response Simulator

Your task is to create a generator function called paginateUsers that simulates fetching users from a database in pages. The generator should meet the following requirements:

  • Input: An array of usernames (strings) and a pageSize (number).
  • Yield: It should yield a page of usernames (an array of strings).
  • Next: It should accept a boolean via .next(). If true, the generator should skip the next page entirely. If false (or undefined), it proceeds as normal.
  • Return: When the generator is exhausted, it should return the total number of pages actually yielded.

Make sure you explicitly type the Generator return type using the Generator<T, TReturn, TNext> generic.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.