Skip to Content
Course content

101: Typing Async Generators

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

I’ve seen this a lot when developers start moving from standard async/await patterns to streams. You want to paginate through a large dataset—say, a list of users from an API—and you think, "An async generator is perfect for this." But then you hit a wall with the types, and suddenly your beautifully typed codebase is leaking any everywhere.

// The "I'll just let TypeScript figure it out" approach
async function* fetchUsers(url: string) {
  let page = 1;
  while (page < 3) {
    const response = await fetch(`${url}?page=${page}`);
    const data = await response.json();
    
    for (const user of data.users) {
      yield user; // What is 'user' actually?
    }
    page++;
  }
  return "Finished fetching all users";
}

async function run() {
  for await (const user of fetchUsers("https://api.example.com/users")) {
    console.log(user.name); // Error: Property 'name' does not exist on type 'unknown'
  }
}

The "Unknown" Leak in your API Stream

If you look at the code above, it feels correct. It runs. But TypeScript is grumpy. Because we didn't explicitly type the generator, it's inferring the yield type as unknown (or any in older configurations). When you try to access user.name, the compiler stops you because it has no idea what the API is actually returning.

You might be tempted to just cast the result inside the for await loop, but that's a band-aid. The logic for what is being streamed belongs inside the generator itself. If you change your API response structure later, you want the error to trigger at the source, not at every single place you consumed the stream.

Taming the AsyncGenerator Generic

To fix this, we need to use the AsyncGenerator type. Now, this is where it gets a bit intimidating because it takes three different generic arguments: AsyncGenerator<T, TReturn, TNext>.

  • T: The type of values the generator yields. This is the one you'll use 90% of the time.
  • TReturn: The type the generator returns when it's completely finished (the value in the final return statement).
  • TNext: The type of value the generator expects to receive if you call .next(value) manually. Most of us just use void here because we rarely "push" values back into a generator.

Here is how we rewrite that user fetcher to be type-safe:

interface User {
  id: string;
  name: string;
}

// We tell TS: Yields User, Returns string, Accepts nothing via .next()
async function* fetchUsers(url: string): AsyncGenerator<User, string, void> {
  let page = 1;
  while (page < 3) {
    const response = await fetch(`${url}?page=${page}`);
    const data = await response.json();
    
    for (const user of data.users) {
      yield user; 
    }
    page++;
  }
  return "Finished fetching all users";
}

async function run() {
  const generator = fetchUsers("https://api.example.com/users");
  
  // Now 'user' is correctly typed as User
  for await (const user of generator) {
    console.log(user.name); 
  }
}

Why the Return Type Matters

You might notice that the for await...of loop actually ignores the return value ("Finished fetching all users"). This is a common point of confusion. The loop only cares about what is yielded.

If you actually need that return value, you can't use a for await loop. You have to manually call .next() until the done property is true. I'll be honest: it's rare to need the return value in a generator, but when you're building complex state machines or data pipelines, that final summary value is a lifesaver. By typing it as AsyncGenerator<User, string, void>, you ensure that whoever calls the generator knows exactly what they're getting at the end of the line.




📋 Practical Task

Build a Type-Safe Log File Processor

You are building a system that reads large log files line-by-line to avoid loading the whole file into memory. Your goal is to create an async generator that processes these logs.

Requirements:

  • Create an interface LogEntry with properties timestamp: string and message: string.
  • Implement an async generator function called processLogs.
  • The generator should yield LogEntry objects.
  • The generator should return a number representing the total count of lines processed.
  • Use the AsyncGenerator type explicitly in the function signature.
  • Write a small main function that consumes the generator and logs the message of each entry.

Hint: Since you aren't actually reading a file, you can simulate the "async" part by using a simple array of strings and a await new Promise(r => setTimeout(r, 10)) inside your loop.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.