TS 5.6のIterator HelpersサポートはBetaと正式版でどう変わったか

皆さんこんにちは。[TypeScript 5.6](https://devblogs.microsoft.com/typescript/announcing-typescript-5-6/)で追加された**Iterator Helpers**のサポートについては以下の記事で紹介しました。

https://qiita.com/uhyo/items/3855efd0d6c00c9f9c0f

しかし、上記の記事はTS 5.6 Beta時点のもので、その中で説明した状態から実装が変更されました。記事のタイトルにある `BuiltinIterators` も別の名前に変わりました。

そこで、この記事ではTS 5.6の正式版でどうなったかについて改めて説明します。

ちなみに、この変更は私がTypeScriptのGitHubリポジトリに変更を提案する[issue](https://github.com/microsoft/TypeScript/issues/59444)を建てたことで行われました。（TSチーム内で元々議論があったかもしれないのでうぬぼれかもしれませんが）

## Iterator Helpersとは

前回の記事でも紹介したので軽く流しますが、Iterator HelpersはECMAScriptの新機能であり、従来機能が乏しかった**イテレータ**にメソッドをたくさん追加するものです。次の例では、イテレータに追加された`filter`メソッドを使用しています。

```ts:Iterator Helpersの例
/**
 * 与えられた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](https://www.typescriptlang.org/play/?target=99#code/GYVwdgxgLglg9mABDKBTATgQzQUQG6pgByIAtgEYYDOAFAFCKJWpQBciAyiwDxhmXoAfHQCUiAN4NE6FiHRJmUAHR5MAGxCpaIpcBhq06GkgC8gxEgCkiAEyITDxAAYRAbjoBfOnQgIqUCzIqewtUAHdOFhoAbQBGABpbRIBmRIAWRIBWAF03b2A4dEQaXzB-RDxEOGBkQ2xUfEISCmpjIJExSUZSqjg1VCU1OABzGjw3RAB6SaTENMAZBkAvxUBspUBVBkAYhkBohk8gA)で調べてみましょう。`filter`にマウスを乗せると次のように表示され、`filter`が`IteratorObject`のメソッドであることが確かめられます。

```ts
(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`メソッドの返り値の型などです。

```ts
const nums = new Set([1, 2, 3, 4, 5]);

const values = nums.values();
//    ^?
```

これは以下のように変遷しました。

```ts
// 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を参照してください。

https://github.com/microsoft/TypeScript/pull/59506

新しく作られたこれらの型は、次のような定義になっています。（型定義ファイルから引用）

```ts
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
// 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`型を直接使うのではなく、より具体的な型を介する定義に変更されました。