The short answer
Give the async function* a @returns {AsyncGenerator<Order, void, undefined>}, and loop over its result with for await:
/**
* @typedef {object} Order
* @property {string} id
* @property {number} total
*/
/**
* @param {number} page
* @returns {Promise<{ orders: Order[], next: number | null }>}
*/
async function fetchPage(page) {
const response = await fetch(`https://shop.example/api/orders?page=${page}`);
return response.json();
}
/** @returns {AsyncGenerator<Order, void, undefined>} */
export async function* allOrders() {
/** @type {number | null} */
let page = 1;
while (page !== null) {
const { orders, next } = await fetchPage(page);
yield* orders;
page = next;
}
}
let revenue = 0;
for await (const order of allOrders()) {
revenue += order.total;
}
console.log(revenue);interface Order {
id: string;
total: number;
}
async function fetchPage(page: number): Promise<{ orders: Order[]; next: number | null }> {
const response = await fetch(`https://shop.example/api/orders?page=${page}`);
return response.json();
}
export async function* allOrders(): AsyncGenerator<Order, void, undefined> {
let page: number | null = 1;
while (page !== null) {
const { orders, next } = await fetchPage(page);
yield* orders;
page = next;
}
}
let revenue = 0;
for await (const order of allOrders()) {
revenue += order.total;
}
console.log(revenue);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Every yield (and yield*) is checked against Order, and the loop variable order is an Order, so order.total is checked at the call site too.
Why it works
An async generator function returns an AsyncGenerator object, which is both an async iterator (it has next(), returning promises) and an async iterable (for await can loop over it). Its three type arguments are what it yields, what it returns when it finishes, and what next() accepts. for await unwraps each promise, so the loop sees plain Order values.
The same thing in TypeScript
The TypeScript equivalent tab writes async function* allOrders(): AsyncGenerator<Order, void, undefined>. Both forms can also leave the annotation out: tsc infers the yield type from the yield expressions.
What goes wrong
Yield something else and the yield is the error (TS2322):
/** @typedef {{ id: string, total: number }} Order */
/** @returns {AsyncGenerator<Order, void, undefined>} */
export async function* allOrders() {
yield { id: "A-1001", total: 40 };
yield "A-1002";error TS2322: Type 'string' is not assignable to type 'Order'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Forget the await in the loop and tsc reports that the generator is not iterable the synchronous way (TS2488):
/** @typedef {{ id: string, total: number }} Order */
/** @returns {AsyncGenerator<Order, void, undefined>} */
export async function* allOrders() {
yield { id: "A-1001", total: 40 };
}
for (const order of allOrders()) {error TS2488: Type 'AsyncGenerator<Order, void, undefined>' must have a '[Symbol.iterator]()' method that returns an iterator.
console.log(order.total);
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Gotchas
@yieldsis ignored. It is a documentation tag from JSDoc; TypeScript does not check it. Put the yield type in@returns.yield*delegates element by element.yield* ordersyields each order of the array, so the array’s element type must match the yield type.for awaitneeds a module or an async function. At the top level it works in an ES module, as in the first example; inside a function, the function must beasync.
Questions
Can I write @returns {AsyncIterable<Order>} instead?
Yes. AsyncIterable<Order> (or AsyncIterableIterator<Order>) is a looser type that hides the generator's return and throw methods from callers; it is enough when callers only loop with for await.
What are the second and third type arguments of AsyncGenerator?
The type the generator returns when it finishes (void here) and the type callers may pass to next() (undefined when nobody does). Synchronous generators use Generator<T, TReturn, TNext> the same way.
Does @yields type what the generator yields?
No. TypeScript does not read @yields; the yield type comes from @returns (or is inferred from the yield expressions).
Sources
- for await...of · MDN Web Docs
- async function* · MDN Web Docs
- TypeScript 3.6 release notes (stricter generators) · TypeScript handbook
Last reviewed 21 October 2026.