typescript / expert
Snippet
Async Pagination with Symbol.asyncIterator
Implementing Symbol.asyncIterator turns a cursor-driven data source into a first-class for-await-of iterable. The async generator stores the cursor on the call stack rather than in instance fields, so concurrent iterations of the same stream stay independent. Consumers see a flat stream of items while the class internally pulls one page at a time.
snippet.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PageStream<T> implements AsyncIterable<T> {constructor(private fetchPage: (cursor?: string) => Promise<{ items: T[]; next?: string }>,) {}async *[Symbol.asyncIterator](): AsyncIterator<T> {let cursor: string | undefined;do {const { items, next } = await this.fetchPage(cursor);for (const item of items) yield item;cursor = next;} while (cursor);}}const ids = new PageStream<number>(async (c) =>c ? { items: [3, 4] } : { items: [1, 2], next: 'p2' },);for await (const id of ids) console.log(id);
Breakdown
1
class PageStream<T> implements AsyncIterable<T> {
Declares that instances expose the AsyncIterable protocol for any element type T.
2
async *[Symbol.asyncIterator](): AsyncIterator<T> {
A computed-name async generator method is the protocol's required entry point.
3
const { items, next } = await this.fetchPage(cursor);
Each iteration awaits a single page and destructures the items plus the optional next cursor.
4
for (const item of items) yield item;
yield inside an async generator hands one item at a time to the for-await-of consumer.
5
for await (const id of ids) console.log(id);
The consumer side stays synchronous-looking even though pages are fetched lazily.