Lesson 62 of 80
AsyncIterable<T> and async generators let you type a stream of values that arrive over time, one await at a time, instead of an array that shows up all at once.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A regular iterator produces values you already have, one at a time. An async iterator produces values you don't have yet — each one might involve a network call, a file read, or any other await — and TypeScript's AsyncIterable<T> and AsyncIterator<T> types let you describe that shape precisely: "this will eventually hand you a T, repeatedly, until it says it's done."
The consuming side pairs with a for await...of loop, which does for async iterables what for...of does for regular ones: pull the next value, await it, bind it, repeat. The producing side is usually an async generator — an async function* — which is by far the easiest way to implement AsyncIterable<T> correctly without hand-writing a .next() method yourself.
interface Page<T> {
items: T[];
nextCursor: string | null;
}
async function fetchPage(cursor: string | null): Promise<Page<{ id: number; name: string }>> {
const url = cursor ? `/api/users?cursor=${cursor}` : "/api/users";
const res = await fetch(url);
return res.json();
}
async function* fetchAllUsers(): AsyncGenerator<{ id: number; name: string }> {
let cursor: string | null = null;
do {
const page = await fetchPage(cursor);
for (const user of page.items) {
yield user; // one item out at a time, no full list ever built in memory
}
cursor = page.nextCursor;
} while (cursor !== null);
}
async function printAllUsers(): Promise<void> {
for await (const user of fetchAllUsers()) {
console.log(user.name); // user: { id: number; name: string } — inferred, no annotation needed
}
}
fetchAllUsers's return type, AsyncGenerator<{ id: number; name: string }>, is itself an AsyncIterable<T> — that's what makes for await...of legal on it. Nothing calls fetchPage again until the consumer asks for the next item, so a caller that only needs the first three users never triggers the fourth network request at all.
AsyncIterable<T> and Promise<T[]> are not the same shape, and the difference is the whole point. A Promise<T[]> resolves once, with everything. An AsyncIterable<T> can start handing you items before it knows how many there will be, and a consumer can stop early (break in a for await loop) without ever fetching the rest.yield inside an async generator does not need its own await. await pauses for a promise to settle; yield pauses for the consumer to ask for the next value. They solve different problems and both can appear in the same function body.for await...of loop early still cleans up. TypeScript's typing reflects that async iterators support a return() method the runtime calls automatically on early exit — useful for closing a file handle or a DB cursor opened inside the generator, but only if you write the cleanup with try/finally around the yield."How would you type a function that streams paginated API results without loading them all into memory first?" tests whether a candidate reaches for AsyncIterable<T> and an async generator, or defaults to Promise<T[]> out of habit — the latter works for a demo but falls over the moment the dataset is actually large.