はじめに (対象読者・この記事でわかること)

この記事は、JavaScript/TypeScriptの基本的な知識がある中級プログラマーを対象にしています。特にfor...ofループを使用したことがあるが、エラー「Symbol.iterator is not a function」に遭遇した経験のある方に最適です。

この記事を読むことで、なぜfor...ofループを使用する際にオブジェクトにSymbol.iteratorを実装する必要があるのか、その背景から具体的な実装方法まで理解できます。また、独自のイテラブルオブジェクトを作成する実践的なスキルを習得できます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 前提となる知識1: JavaScript/TypeScriptの基本的な文法とデータ型 前提となる知識2: forループとwhileループの基本的な使い方

for...of文とSymbol.iteratorの概要

JavaScriptおよびTypeScriptにおいてfor...of文は、反復可能なオブジェクト(イテラブルオブジェクト)の要素を順に処理するための便利な構文です。しかし、すべてのオブジェクトがfor...of文で直接使用できるわけではありません。

for...of文が動作するためには、対象のオブジェクトがSymbol.iteratorというプロパティを持っている必要があります。このプロパティはイテレータオブジェクトを返す関数で、イテレータオブジェクトはnext()メソッドを持っています。next()メソッドは、反復処理の各ステップでオブジェクトの次の要素と反復処理の完了状態を示すオブジェクトを返します。

この仕組みにより、配列、Map、Set、文字列など、多くの組み込みオブジェクトはデフォルトでfor...of文を使用できます。しかし、独自のオブジェクトをfor...of文で使用したい場合には、このSymbol.iteratorプロパティを明示的に実装する必要があります。

独自のイテラブルオブジェクトの実装方法

ここでは、独自のイテラブルオブジェクトを実装し、for...of文で使用する具体的な方法をステップバイステップで解説します。

ステップ1: イテラブルオブジェクトの基本構造の定義

まず、TypeScriptでイテラブルオブジェクトの基本構造を定義します。以下に簡単なカウンターの例を示します。

Typescript
// イテレータインターフェースの定義 interface Iterator<T> { next(): IteratorResult<T>; } // イテレータ結果インターフェースの定義 interface IteratorResult<T> { value: T; done: boolean; } // イテラブルオブジェクトの基本構造 interface Iterable<T> { [Symbol.iterator](): Iterator<T>; }

ステップ2: カスタムイテラブルオブジェクトの実装

次に、具体的なカスタムイテラブルオブジェクトを実装します。以下は、指定された回数だけカウントアップするオブジェクトの例です。

Typescript
// カスタムイテラブルオブジェクトの実装 class Counter implements Iterable<number> { private count: number; private max: number; constructor(max: number) { this.count = 0; this.max = max; } // Symbol.iteratorメソッドの実装 [Symbol.iterator](): Iterator<number> { return { next: (): IteratorResult<number> => { if (this.count < this.max) { const value = this.count++; return { value, done: false }; } else { return { value: undefined, done: true }; } } }; } }

ステップ3: イテラブルオブジェクトの使用

実装したカスタムイテラブルオブジェクトをfor...of文で使用します。

Typescript
// イテラブルオブジェクトの使用例 const counter = new Counter(5); for (const num of counter) { console.log(num); // 0, 1, 2, 3, 4 が順に出力される }

ステップ4: ジェネレータを使った簡潔な実装

上記の実装は少し冗長に感じるかもしれません。JavaScript/TypeScriptでは、ジェネレータ関数を使うことで、より簡潔にイテラブルオブジェクトを実装できます。

Typescript
// ジェネレータ関数を使った実装 class SimpleCounter implements Iterable<number> { private max: number; constructor(max: number) { this.max = max; } // ジェネレータ関数を使ったSymbol.iteratorの実装 *[Symbol.iterator](): Generator<number> { for (let i = 0; i < this.max; i++) { yield i; } } } // 使用例 const simpleCounter = new SimpleCounter(3); for (const num of simpleCounter) { console.log(num); // 0, 1, 2 が順に出力される }

ステップ5: 非同期イテラブルオブジェクトの実装

さらに、非同期処理を含むイテラブルオブジェクト(非同期イテラブルオブジェクト)を実装することもできます。これは非同期処理の結果を順に処理する場合に便利です。

Typescript
// 非同期イテレータインターフェースの定義 interface AsyncIterator<T> { next(): Promise<IteratorResult<T>>; } // 非同期イテラブルオブジェクトのインターフェース interface AsyncIterable<T> { [Symbol.asyncIterator](): AsyncIterator<T>; } // 非同期イテラブルオブジェクトの実装 class AsyncCounter implements AsyncIterable<number> { private max: number; private delay: number; constructor(max: number, delay: number) { this.max = max; this.delay = delay; } // 非同期Symbol.iteratorメソッドの実装 async *[Symbol.asyncIterator](): AsyncGenerator<number> { for (let i = 0; i < this.max; i++) { await new Promise(resolve => setTimeout(resolve, this.delay)); yield i; } } } // 使用例 async function main() { const asyncCounter = new AsyncCounter(5, 1000); // 1秒ごとにカウントアップ for await (const num of asyncCounter) { console.log(num); // 1秒ごとに 0, 1, 2, 3, 4 が順に出力される } } main();

ハマった点やエラー解決

エラー1: 'Symbol.iterator' is not defined

TypeScriptでSymbol.iteratorを使用しようとすると、このエラーが発生することがあります。これはTypeScriptの設定が原因であることが多いです。

Typescript
// エラーが発生するコード class MyIterable { [Symbol.iterator]() { // エラー: 'Symbol.iterator' is not defined return { next: () => ({ value: 1, done: false }) }; } }

解決策1: ES2015以上のターゲットを指定する

tsconfig.jsontargetをES2015以上に設定します。

Json
{ "compilerOptions": { "target": "ES2015", // その他のオプション... } }

解決策2: グローバルのSymbolを使用する

グローバルのSymbolを使用するように明示的に指定します。

Typescript
class MyIterable { [Symbol.iterator]() { // OK return { next: () => ({ value: 1, done: false }) }; } }

エラー2: オブジェクトは反復可能ではありません

for...of文を使用しようとした際に「オブジェクトは反復可能ではありません」というエラーが発生することがあります。

Typescript
// エラーが発生するコード const myObject = { a: 1, b: 2, c: 3 }; for (const value of myObject) { // エラー: オブジェクトは反復可能ではありません console.log(value); }

解決策: Symbol.iteratorを実装する

オブジェクトにSymbol.iteratorを実装します。

Typescript
const myObject = { a: 1, b: 2, c: 3, *[Symbol.iterator]() { // OK for (const key in this) { if (this.hasOwnProperty(key)) { yield this[key]; } } } }; for (const value of myObject) { console.log(value); // 1, 2, 3 が順に出力される }

エラー3: next()メソッドが正しく実装されていない

イテレータを実装する際に、next()メソッドの戻り値が不正な形式であるとエラーが発生します。

Typescript
// エラーが発生するコード class InvalidIterator { [Symbol.iterator]() { return { next: () => ({ value: 1 }) // doneプロパティが不足 }; } }

解決策: next()メソッドの戻り値を修正する

next()メソッドがvaluedoneプロパティを持つオブジェクトを返すように修正します。

Typescript
class ValidIterator { [Symbol.iterator]() { return { next: () => ({ value: 1, done: false }) // OK }; } }

まとめ

本記事では、TypeScriptでfor...of文を使用するためにSymbol.iteratorを実装する方法について解説しました。

この記事を通して、独自のイテラブルオブジェクトを実装する実践的なスキルを習得できたと思います。今後は、より複雑なデータ構造を扱う際に、この知識を活用してみてください。

参考資料

参考にした記事、ドキュメント、書籍などがあれば、必ず記載しましょう。