Skip to main content

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 AsyncGenerator and for 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 iteratorsAsyncGenerator, 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.

Footnotes

  1. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). Iterator is catalogued there as a behavioral pattern. The summary of the intent is this guide's own; we have not checked it against the original text.

  2. Source: Iteration protocols (MDN Web Docs). 2 3

  3. Source: Async Iterator Helpers (TC39 proposal). It is a separate proposal split off from the synchronous proposal-iterator-helpers.