本稿では、TypeScriptにてfor ( ... of ... )
でループできるクラスの作り方を説明する。
やりたいこと
やりたいこととしては、次のような自作のクラスがあって、
class MyList {
constructor(private elements: Array<number>) {}
}
これをnew
したオブジェクトを、
const list = new MyList([1, 2, 3])
まるでArray
のようにfor (... of ...)
でループできるようにしたい。
for (const element of list) {
// ...
}
もちろんこのままじゃコンパイルエラーになります。
JavaScriptのオブジェクトはどういう基準でループできるのか?
まず、JavaScriptがfor-of
でループできるものが何なのか、知っておく必要がある。
ビルトインのArray
やMap
は、「反復処理プロトコル(itetration protocol)」に準拠したオブジェクトになっているためループできる。
つまり、自前のオブジェクトでも「反復処理プロトコル」に準拠してさえいれば、ループできるというわけである。
反復処理プロトコルを自前クラスに組み込む
反復処理プロトコルを自前クラスに組み込むには、[Symbol.iterator]()
というメソッドを生やせば良い。
class MyList {
constructor(private elements: Array<number>) {}
[Symbol.iterator]() {}
}
このメソッドの戻り値の型はIterator<T>
にならないといけない。
class MyList {
constructor(private elements: Array<number>) {}
[Symbol.iterator](): Iterator<number> {}
}
this.elements
はArray<number>
だが、先述したとおりArray<T>
も反復処理プロトコルを実装しているので、[Symbol.iterator]
メソッドを持っている。
つまり、次のように、this.elements[Symbol.iterator]()
の戻り値をそのままreturn
してしまえば、コンパイルが通るようになる。
class MyList {
constructor(private elements: Array<number>) {}
[Symbol.iterator](): Iterator<number> {
return this.elements[Symbol.iterator]()
}
}
最後に、クラス自体もIterable<number>
をimplements
しておくといい。無くても動くが、コンパイル時にメソッドのシグネチャのチェックがなされるので、安心度が高まる。
class MyList implements Iterable<number> {
constructor(private elements: Array<number>) {}
[Symbol.iterator](): Iterator<number> {
return this.elements[Symbol.iterator]()
}
}
このコードは試しにTypeScript playgroundで動かしてみることができる。
込み入った実装
上記では話をシンプルにするためにArray<T>
の[Symbol.iterator]
メソッドを呼んで返すだけの実装にしたが、自前クラスに反復処理プロトコルを実装する背景には、複雑なループを隠蔽したいことがあると思う。
そこで、もう少し込み入ったイテレータの実装をサンプルとして考えてみる。
次のサンプルは、偶数だけをループする実装になる:
class MyList implements Iterable<number> {
constructor(private elements: Array<number>) {}
*[Symbol.iterator](): Iterator<number> {
for (const element of this.elements) {
if (element % 2 == 0)
yield element
}
}
}
const list = new MyList([1, 2, 3, 4, 5, 6])
for (const element of list) {
console.log(element) // 2, 4, 6のみが出力される
}
このコードはTypeScript Playgroundで動かしてみれる。
[Symbol.iterator]
メソッドの頭についている*
はジェネレーター関数のマークで、yield
が使えるようになる。ジェネレーターはTypeScriptでは
interface Generator extends Iterator<any> { }
と定義されており、イテレータとして扱うことができる。