メインコンテンツまでスキップ

Iterator — 言語に取り込まれたパターン

言語機能としての反復と、パターンとしての Iterator の関係を扱います。

このパターンが解こうとした問題

GoF は Iterator を、集合の要素へ内部構造を露出せずに順にアクセスする手段として収録しています1

配列・連結リスト・木のように内部の持ち方が違っても、「先頭から順に取り出す」という操作は共通です。この共通の入り口を切り出したのが Iterator です。

このパターンは、対応する仕組みが言語仕様に取り込まれた数少ない例です。 JavaScript の反復規約は、内部の持ち方を見せずに要素へ順にアクセスするという、Iterator と同じ役割を担っています。

クラスで素直に書く

ページ分割された API の走査を題材にします。1 ページずつ取得しながら、呼び出し側には要素の並びだけを見せたいとします。

type Article = {id: string; title: string};
type Page = {items: Article[]; nextCursor: string | null};

declare function fetchPage(cursor: string | null): Promise<Page>;

// 反復の手続きを自前のクラスに閉じ込めた形
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;
}

動きますが、collectTitlesfor 文が読みにくくなっています。バッファの管理、終端の判定、カーソルの持ち回りがすべて手書きです。

言語機能で置き換える

JavaScript には反復のための規約があります。MDN はこう定めています。

A zero-argument function that returns an object, conforming to the iterator protocol.2

イテレーター側は next() が結果を返します。MDN は next() を「A function that accepts zero or one argument and returns an object conforming to the IteratorResult interface」と定めています2

上の ArticleIterator が手で書いていたのは、まさにこの規約です。**規約に沿った名前を付けるだけで、for...of が使えるようになります。**非同期の並びには対になる規約 ([Symbol.asyncIterator]) があり、そちらが for await...of を支えます2。さらに、ジェネレーター構文を使えば状態管理そのものを書かずに済みます。

type Article = {id: string; title: string};
type Page = {items: Article[]; nextCursor: string | null};

declare function fetchPage(cursor: string | null): Promise<Page>;

// バッファも終端フラグも書かない。yield が中断と再開を担う
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;
}

クラス、バッファ、終端フラグ、shift() がすべて消えました。呼び出し側も for await...of の 1 行です。articles() が返すものは反復規約を満たしているので、分割代入やスプレッドなど言語の他の機能ともそのままつながります。

自分のクラスを反復できるようにしたい場合も、規約に沿ったメソッドを 1 つ足すだけです。

type Article = {id: string; title: string};

class ArticleFeed {
constructor(private readonly items: Article[]) {}

// このメソッドがあるだけで for...of とスプレッドが使えるようになる
*[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);

本連載の判断

判定: 言語機能で代替できる。 反復のために独自のインターフェースを設計する理由は、TypeScript では見当たりません。

  • 順に取り出す処理を書くなら、まずジェネレーター — 状態の管理を言語が引き受けます。遅延評価も自然に得られ、必要な分だけ取り出して途中でやめられます。
  • 自分の型を反復可能にするなら [Symbol.iterator] — 独自のメソッド名を決めるより、規約に乗るほうが呼び出し側の選択肢が増えます。
  • 非同期の並びなら AsyncGeneratorfor await...of — ページ分割 API やストリームがここに当たります。

独自の反復インターフェースを設計すると、言語側の構文と接続できなくなります。得るものがなく失うものだけがあるので、この判定は他の章より強く出せます。

この判定が覆る条件

ジェネレーターが返したオブジェクトは一度きりです。回し終えたものをもう一度回すことはできません。上の articles() のように「呼ばれるたびに新しいジェネレーターを返す関数」にしておくか、ArticleFeed のように [Symbol.iterator] を持つ値にしておけば、何度でもたどれます。逆に、生成済みのジェネレーターを変数に入れて配り回すと、受け取った側の 1 つだけが並びを消費します。この違いは型に現れないので、どちらの形で渡すかを設計時に決めておく必要があります。

もう 1 つ、ES2025 で入った反復のためのメソッド (map / filter / take など) を使うと、途中の配列を作らずに書けます。ただし条件が 2 つあります。libES2025 以上を含めないと型が付きません (本ガイドの検証環境の既定は ES2022 です)。そして これらは同期のイテレーターにしか生えません — 本章の主役である AsyncGenerator には map がなく、非同期版は別の提案として分かれています3。実行環境側の対応状況もあわせて確認してください。

既存記事との関係

  • 繰り返し処理forEach / map / filter / reduce の使い分けを扱っています。あちらは配列に対する処理の選び方、本章は反復規約そのものの話です。

ジェネレーター構文そのものを扱った記事は、本サイトにはまだありません。 本章では function*yield*async function* を使いましたが、構文の詳しい説明は付けていません。必要なら MDN の Iteration protocols と、そこからたどれる function* の項を参照してください。

Footnotes

  1. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides『Design Patterns: Elements of Reusable Object-Oriented Software』(Addison-Wesley, 1994)。Iterator は振る舞いに関するパターンとして収録されています。意図の要約は本ガイドによるもので、原典とは照合していません。

  2. 出典: Iteration protocols(MDN Web Docs)。 2 3

  3. 出典: Async Iterator Helpers(TC39 proposal)。同期版の proposal-iterator-helpers から分離された別提案です。