皆さんこんにちは。TypeScript 5.6で追加されたIterator Helpersのサポートについては以下の記事で紹介しました。
しかし、上記の記事はTS 5.6 Beta時点のもので、その中で説明した状態から実装が変更されました。記事のタイトルにある BuiltinIterators も別の名前に変わりました。
そこで、この記事ではTS 5.6の正式版でどうなったかについて改めて説明します。
ちなみに、この変更は私がTypeScriptのGitHubリポジトリに変更を提案するissueを建てたことで行われました。(TSチーム内で元々議論があったかもしれないのでうぬぼれかもしれませんが)
Iterator Helpersとは
前回の記事でも紹介したので軽く流しますが、Iterator HelpersはECMAScriptの新機能であり、従来機能が乏しかったイテレータにメソッドをたくさん追加するものです。次の例では、イテレータに追加されたfilterメソッドを使用しています。
/**
* 与えられたSet<number>のうち、偶数のみを出力するイテレータを返す
*/
function iterateEvenNumbers(
set: Set<number>
) {
return set.values().filter(n => n % 2 === 0);
}
const nums = new Set([1, 2, 3, 4, 5]);
for (const v of iterateEvenNumbers(nums)) {
console.log(v); // 2, 4が出力される
}
IteratorObject
TypeScript 5.6ではIteratorObjectという型が追加されました。TS 5.6 BetaでBuiltinIteratorと呼ばれていたものが改名されました。
IteratorObjectは「Iterator Helpersを持つイテレータオブジェクト」を表す型です。言い換えれば、ランタイムにIterator.prototypeをプロトタイプに持つオブジェクトだと思っても差し支えないでしょう。
つまり、上の例で使われているfilterメソッドはIteratorObject型に由来するメソッドです。上の例をPlaygroundで調べてみましょう。filterにマウスを乗せると次のように表示され、filterがIteratorObjectのメソッドであることが確かめられます。
(method) IteratorObject<number, undefined, unknown>.filter(predicate: (value: number, index: number) => unknown): IteratorObject<number, undefined, unknown> (+1 overload)
TypeScriptでは「Fooというクラスのインスタンスであるオブジェクトの型はFoo」とするのが原則ですが、ここでは「Iteratorというクラスのインスタンスであるオブジェクトの型はIteratorObject」となっており原則から外れています。これは前回の記事で説明した通り、Iterator型はすでに別の意味でTypeScriptに存在していたからです。
ビルトインメソッドが返すイテレータの型
TS 5.6 Betaから正式版までに変わった点として、ビルトインメソッドが返すイテレータの型の表現があります。具体的には、Setのvaluesメソッドの返り値の型などです。
const nums = new Set([1, 2, 3, 4, 5]);
const values = nums.values();
// ^?
これは以下のように変遷しました。
// TS 5.5
IterableIterator<number>
// TS 5.6 Beta
BuiltinIterator<number, BuiltinIteratorReturn, any>
// TS 5.6
SetIterator<number>
TS 5.6では、どのビルトインメソッドから返されたのかに対応した型が用意されるようになりました。この例では、Setのメソッドが返したイテレータだからSetIteratorです。ECMAScriptの範囲では、他にもArrayIterator, MapIterator, StringIterator, RegExpStringIterator, SegmentIteratorが存在します。DOM由来のイテレータの型もたくさん定義されました。詳しくは実装のPRを参照してください。
新しく作られたこれらの型は、次のような定義になっています。(型定義ファイルから引用)
interface SetIterator<T> extends IteratorObject<T, BuiltinIteratorReturn, unknown> {
[Symbol.iterator](): SetIterator<T>;
}
ポイントは、IteratorObject<T, BuiltinIteratorReturn, unknown>のところです。
BuiltinIteratorReturnはどうなった
BuiltinIteratorReturnについても前回の記事で解説しました。この型は、TS 5.6で追加されたstrictBuiltinIteratorReturnコンパイラオプションが有効化どうかによって中身が変わります。有効時はunknown、無効時はanyです。
元々Iteratorの第2型引数(TReturn)のデフォルト値がanyになっている問題がありました。せめてビルトインのイテレータ(ビルトインメソッドが返す、今でいうIteratorObjectに相当する型)ではTReturnがunknownになるように改善したいというモチベーションで作られたものです。一応後方互換性の問題があるため、コンパイラオプションとセットで導入となっています。
上のような定義になっているため、SetIterator<T>などではTReturnはBuiltinIteratorReturnになります。
この辺りの型定義の取り扱いが、TS 5.6 Betaから正式版の間に特に改善されたところです。
Setのfilterメソッドの定義をTS 5.6 Betaと正式版で見比べてみましょう。
// TS 5.6 Beta
values(): BuiltinIterator<T, BuiltinIteratorReturn>;
// TS 5.6
values(): SetIterator<T>;
このように、5.6 Betaの時点では、BuiltinIterator(今でいうIteratorObject)の第2型引数をBuiltinIteratorReturnに指定しており、BuiltinIteratorを返す全箇所で明示的にBuiltinIteratorReturnと書く定義になっていました。
筆者が建てたissueで指摘していた問題は、BuiltinIteratorの第2型引数のデフォルト値がanyのままであり、型定義を明記するときにうっかりBuiltinIterator<T>と書いてしまうとBuiltinIteratorReturnが使われなくなってしまう(せっかくstrictBuiltinIteratorReturnオプションが有効になっていてもその恩恵が消えてしまう)ということです。
TS 5.6の正式版では型はSetIterator<T>のようにBuiltinIteratorReturnが表面には出てこない定義になっており、この問題が解消されています。
ちなみに、筆者は最初BuiltinIteratorの第2型引数TReturnのデフォルト値をBuiltinIteratorReturnにしてしまえばいいのでは? という提案をしたのですが、それはできませんでした。それは、BuiltinIteratorReturnはジェネレータ関数の型推論にも関わっており、その場合第2型引数がvoidとなりBuiltinIteratorReturnとかみ合わないからです。
TS 5.6の最終的な型定義では、ビルトインのメソッドが返すイテレータに個別の型名を与えることで、BuiltinIteratorReturnを型引数のデフォルトにしないことと、型名の表面に現れないことを両立しています。
まとめ
TS 5.6のIterator Helpersサポートは待望の機能で、イテレータの利便性を大きく向上してくれます。
最終的な形になるまでには、この記事で説明したような試行錯誤(?)と変遷がありました。
特に、Beta時点でBuiltinIterator型だったものはIteratorObject型に改名され、ビルトインのメソッドが返すイテレータの型定義はIteratorObject型を直接使うのではなく、より具体的な型を介する定義に変更されました。