Iterator — The Pattern the Language Absorbed
This chapter covers the relationship between iteration as a language feature and Iterator as a pattern.
The problem this pattern set out to solve
GoF catalogs Iterator as a way to access the elements of a collection in order without exposing its internal structure1.
Arrays, linked lists, and trees hold their contents differently, yet "take them one at a time from the front" is a shared operation. Iterator is that shared entry point factored out.
This pattern is one of the few cases where the corresponding mechanism was absorbed into the language specification. JavaScript's iteration protocols carry the same role as Iterator: accessing elements in order without showing how they are held internally.
Writing it straightforwardly with classes
The subject here is walking a paginated API. You fetch one page at a time while showing the caller nothing but a sequence of elements.
type Article = {id: string; title: string};
type Page = {items: Article[]; nextCursor: string | null};
declare function fetchPage(cursor: string | null): Promise<Page>;
// The iteration procedure sealed inside a class of your own
class ArticleIterator {
private buffer: Article[] = [];
private cursor: string | null = null;
private done = false;
async next(): Promise<{value: Article; done: false} | {value: undefined; done: true}> {
while (this.buffer.length === 0 && !this.done) {
const page = await fetchPage(this.cursor);
this.buffer = page.items;
this.cursor = page.nextCursor;
this.done = page.nextCursor === null;
}
const value = this.buffer.shift();
return value === undefined ? {value: undefined, done: true} : {value, done: false};
}
}
async function collectTitles(): Promise<string[]> {
const iterator = new ArticleIterator();
const titles: string[] = [];
for (let result = await iterator.next(); !result.done; result = await iterator.next()) {
titles.push(result.value.title);
}
return titles;
}
It works, but the for statement in collectTitles has become hard to read. Managing the buffer, deciding when the end arrives, and carrying the cursor are all written by hand.
Replacing it with language features
JavaScript has protocols for iteration. MDN defines them this way.
A zero-argument function that returns an object, conforming to the iterator protocol.2
On the iterator's side, next() returns the result. MDN defines next() as "A function that accepts zero or one argument and returns an object conforming to the IteratorResult interface"2.
What ArticleIterator above wrote by hand is exactly that protocol. Just naming things the way the protocol expects makes for...of work. Asynchronous sequences have a paired protocol ([Symbol.asyncIterator]) that backs for await...of2. And with generator syntax, the state management goes away entirely.
type Article = {id: string; title: string};
type Page = {items: Article[]; nextCursor: string | null};
declare function fetchPage(cursor: string | null): Promise<Page>;
// No buffer and no done flag. yield handles suspending and resuming
async function* articles(): AsyncGenerator<Article> {
let cursor: string | null = null;
do {
const page = await fetchPage(cursor);
yield* page.items;
cursor = page.nextCursor;
} while (cursor !== null);
}
async function collectTitles(): Promise<string[]> {
const titles: string[] = [];
for await (const article of articles()) {
titles.push(article.title);
}
return titles;
}
The class, the buffer, the done flag, and shift() are all gone. The calling side is one line of for await...of. What articles() returns satisfies the iteration protocol, so it connects directly to other language features such as destructuring and spread.
Making a class of your own iterable also takes just one method that follows the protocol.
type Article = {id: string; title: string};
class ArticleFeed {
constructor(private readonly items: Article[]) {}
// Just having this method makes for...of and spread work
*[Symbol.iterator](): Generator<Article> {
yield* this.items;
}
}
const feed = new ArticleFeed([
{id: '1', title: 'Singleton'},
{id: '2', title: 'Builder'},
]);
const titles = [...feed].map((article) => article.title);
How this series judges it
Verdict: language features can replace it. In TypeScript there is no visible reason to design an interface of your own for iteration.
- Writing something that takes items in order means reaching for a generator first — the language takes over the state management. Lazy evaluation comes naturally too, so you can take only what you need and stop partway.
- Making your own type iterable means
[Symbol.iterator]— riding the protocol gives the caller more options than picking a method name of your own. - An asynchronous sequence means
AsyncGeneratorandfor await...of— paginated APIs and streams land here.
Designing an iteration interface of your own cuts you off from the language's syntax. There is nothing to gain and only something to lose, which is why this verdict can be stated more firmly than in other chapters.
What would overturn this judgment
The object a generator returns is single-use. Once you have walked it, you cannot walk it again. Keep it as a function that returns a fresh generator on each call, as articles() does, or as a value carrying [Symbol.iterator], as ArticleFeed does, and you can walk it as many times as you like. Put an already-created generator in a variable and hand it around, on the other hand, and exactly one recipient consumes the sequence. That difference does not show up in the type, so which form you hand out is something to settle at design time.
One more thing: the iteration methods added in ES2025 (map / filter / take and others) let you write this without building intermediate arrays. Two conditions apply, though. They come untyped unless lib includes ES2025 or later (the default in this guide's verification environment is ES2022). And they only grow on synchronous iterators — AsyncGenerator, the star of this chapter, has no map, and the asynchronous version is split off as a separate proposal3. Check runtime support as well.
How this relates to existing articles
- Loops covers choosing among
forEach/map/filter/reduce. That article is about picking an operation on arrays; this chapter is about the iteration protocol itself.
This site has no article on generator syntax itself yet. This chapter uses function*, yield*, and async function* without explaining the syntax in detail. If you need that, see MDN's Iteration protocols and the function* entry you can reach from it.