TS 5.6のiterator helpersサポート――BuiltinIteratorsのwhat & whyを知る

先日、TypeScript 5.6 Betaが公開され、あわせてリリースノートも出ました。

https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/

この記事では、TS 5.6の新機能の中でも**Iterator helpers**に注目します。特に、Iterator helpersのサポートに合わせて型定義に追加された`BuiltinIterators`や`BuiltinAsyncIterators`について解説します。

:::note alert
この記事の内容は**TS 5.6 Beta**の時点のものです。TS 5.6の正式版では型名などに修正が入り、この記事の内容は古くなりました。

この記事は当時の状態や歴史を知るためにこのまま残してあります。最新の状況は以下の記事を参照してください。

[TS 5.6のIterator HelpersサポートはBetaと正式版でどう変わったか](https://qiita.com/uhyo/items/32aab57b4ccd8ab60c80)
:::

## Iterator helpersとは

**Iterator helpers**は、[ECMAScriptのプロポーザル](https://github.com/tc39/proposal-iterator-helpers)のひとつであり、この記事の公開時点ではStage 3という完成目前の状態にあります。TypeScriptではStage 3に到達したプロポーザルが実装されるので今回これが実装されることになりました。

ランタイムの実装としては、この記事公開時点ではGoogle Chrome 122, Node.js 22.0.0, Deno 1.42などに実装されています。

Iterator helpersは、**イテレータ**にいくつかのメソッドが追加されて便利になるというものです。

イテレータは、すごく大ざっぱに言えば配列を抽象化して遅延評価を加えたものです。イテレータはES2015で導入され、それ以降の言語仕様ではイテレータが積極的に用いられています。例えば、配列を受け取るように見える関数が実はイテレータを受け付けるということも多くあります。

```ts:イテレータの使用例
/**
 * start以上end以下の数を順に出力するイテレータを返す
 */
function* range(start: number, end: number): Generator<number> {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

// イテレータはfor-of文に渡して使うことができる
for (const v of range(0, 10)) {
  console.log(v);
}

// new Setも実は配列だけでなくイテレータを受け取ることができる

const nums = new Set(range(0, 10));

console.log(nums); // Set (11) {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
```

また、組み込み関数がイテレータを返す場合もあります。例えば、`Set`の`values`メソッドはイテレータを返します。イテレータはそのままfor-of文などに渡すこともできますが、配列の形でほしい場合は`Array.from`で配列にできます。

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

const values = nums.values();
console.log(values); // SetIterator オブジェクト

const array = Array.from(values);
console.log(array); // [1, 2, 3, 4, 5]
```

従来、イテレータは`next`以外にメソッドを持っておらず、不便な存在でした。配列のfilterメソッドのような加工が不可能で、そのような操作を行いたければイテレータを一度配列にするか、あるいはジェネレータ関数を駆使して手書きで実装する必要がありました。

Iterator helpersでは、イテレータ自体にメソッドが追加され、イテレータの利便性が向上します。具体的には、以下のメソッドが追加されます。

- drop
- every
- filter
- find
- flatMap
- forEach
- map
- reduce
- take
- toArray

drop, take, toArray以外は配列に同名のメソッドがあるので理解しやすいでしょう。dropとtakeは敢えて配列のメソッドと比べるとsliceが近いですね。toArrayは、従来`Array.from`でできていたイテレータの配列化がメソッドになったものです。

いくつか具体例を見てみましょう。

```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が出力される
}
```

この例は、配列が出てきません（`Set`を作るところは本題に関係ないので許してください）。`Set`の中身をvaluesで取り出してから最終的にfor-of文に入力するまで、イテレータだけで完結しています。

従来であればこうはいかず、次のようにする必要があったでしょう。この例では、関数の返り値をイテレータにするのを諦めて配列にしています。

```ts:従来例1
/**
 * 与えられたSet<number>のうち、偶数のみを出力する配列を返す
 */
function getEvenNumbers(
  set: Set<number>
) {
  return Array.from(set.values()).filter(n => n % 2 === 0);
}

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

for (const v of getEvenNumbers(nums)) {
  console.log(v); // 2, 4が出力される
}
```

どうしても配列を介したくない場合は、ジェネレータ関数を使う次のようなやり方になるでしょう。このやり方では、`filter`のような便利なメソッドが使えずif文の手書きになってしまいました。

```ts:従来例2
/**
 * 与えられたSet<number>のうち、偶数のみを出力するイテレータを返す
 */
function* iterateEvenNumbers(
  set: Set<number>
) {
  for (const v of set) {
    if (v % 2 === 0) {
      yield v;
    }
  }
}

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

for (const v of iterateEvenNumbers(nums)) {
  console.log(v); // 2, 4が出力される
}
```

ジェネレータ関数を使いたくないけど配列にもしたくない場合は、もう`iterateEvenNumbers`の関数化を諦めるしかありません。

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

for (const v of nums) {
  if (v % 2 === 0) {
    console.log(v); // 2, 4が出力される
  }
}
```

ということで、従来例と比較すると、iterator helpersが「配列を介さない」ことと「きれいで分かりやすいコード」を両立するために有用であることが分かりますね。

## IteratorとIterable

ここからこの記事の本題に入っていきます。まずは、TS 5.6より前からあった用語を整理します。具体的には、**Iterator**と**Iterable**です。

**Iterator**（イテレータ）は、JavaScriptにおけるイテレータ本体であるオブジェクトのことです。Iteratorは、`next`メソッドを持つのが特徴です。例えば、先ほどから使ってきたSetの`values`の返り値はイテレータであり、`next`メソッドを持っています。

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

const iterator = nums.values(); // これがIterator

console.log(iterator.next); // function next() { [native code] } 
```

イテレータの中身に対してループするときはfor-of文を使うのが主流でしたが、手動で`next`メソッドを呼び出すことによってもループをするのは一応可能です。

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

const iterator = nums.values(); // これがIterator

while (true) {
  const result = iterator.next();
  if (result.done) {
    break;
  }
  console.log(result.value); // 1, 2, 3, 4, 5 が出力される
}
```

for-of文も、内部で上記のようなwhile文とだいたい同じような挙動をしています。

**Iterable**は、「イテレータを生み出すことができるオブジェクト」であり、`[Symbol.iterator]`メソッドを持つことが特徴です。例えば、配列やSetはIterableです。

よく考えてみると、次のようにfor-of文に配列を渡すことができますが、配列はイテレータではありません（nextメソッドを持ちません）。

```ts
const arr = [1, 2, 3, 4, 5];

console.log(arr.next); // undefined

for (const v of arr) { // でも配列はfor-of文でループできる
  console.log(v);
}
```

実は、for-of文に渡すことができるのはイテレータではなくIterableなのです。Iterableが渡されたら、内部的にイテレータが作成され、それに対してループが行われるのです。手動のwhile文で表すとこういう感じです。

```ts
const arr = [1, 2, 3, 4, 5];

const iterator = arr[Symbol.iterator](); // Iterableからイテレータを得る

while (true) {
  const result = iterator.next();
  if (result.done) {
    break;
  }
  console.log(result.value); // 1, 2, 3, 4, 5 が出力される
}
```

以上のコードではSetや配列など組み込みのオブジェクトに頼ってイテレータを作っていましたが、イテレータ及びIterableのルールに従っていれば、独自にイテレータやIterableを作ることも可能です。

```ts
const oreoreIterable = {
  [Symbol.iterator]() {
    // Iteratorを作って返す
    let current = 1;
    return {
      next() {
        if (current <= 5) {
          return {
            done: false,
            value: current++
          };
        }
        return {
          done: true,
          value: undefined,
        };
      }
    };
  }
}

for (const v of oreoreIterable) {
  console.log(v); // 1, 2, 3, 4, 5 が出力される
}
```

このように、従来、IteratorやIterableというのはそういうクラスがあるわけではなく、インターフェースを指す言葉でした。このことを指して「Iteratorプロトコル」のように言うこともあります。

ちなみに、なぜIterableとIteratorが分かれているのかについても、上の例を見れば少し分かるのではないでしょうか。Iterable (`oreoreIterable`) は状態を持たないイミュータブルなオブジェクトですが、イテレータ（`[Symbol.iterator]`の返り値）は`this.current`という状態を持つオブジェクトです。イテレータは`next`メソッドが呼ばれるたびに次の値を返すというインターフェースになっているため、必然的に状態を持ちます。配列やSetといったものがIterableに相当すると考えると、それらに対する1回のイテレーションを担当するイテレータオブジェクトは別に作られる必要があることが分かります。

## TypeScriptにおけるIteratorとIterable

ここからやっとTypeScriptの話題に入ります。TypeScriptにおいては、`Iterator`も`Iterable`も型名として存在しています。これらの型名を先ほどのコードに付加するとこのようになります。

```ts
const oreoreIterable: Iterable<number> = {
  [Symbol.iterator](): Iterator<number> {
    // Iteratorを作って返す
    let current = 1;
    return {
      next() {
        if (current <= 5) {
          return {
            done: false,
            value: current++
          };
        }
        return {
          done: true,
          value: undefined,
        };
      }
    };
  }
}
```

また、「俺は直接Iteratorを作りたいんや！！　Iterableなんか要らん！！」という人のために、`Iterator`でもあり`Iterable`でもあることを表す `IterableIterator`も用意されています。

```ts
let current = 1;

const oreoreItertor: IterableIterator<number> = {
  [Symbol.iterator]() {
    return this;
  },
  next() {
    if (current <= 5) {
      return {
        done: false,
        value: current++
      };
    }
    return {
      done: true,
      value: undefined,
    };
  }
};

for (const v of oreoreItertor) {
  console.log(v); // 1, 2, 3, 4, 5 が出力される
}
```

このように、Iteratorを直接作ってfor-of文とかに渡したい場合に、`this`を返す`[Symbol.iterator]`メソッドを用意して渡せるようにするのが`IterableIterator`です。関数がIteratorを返す場合などに`IterableIterator`の概念は有効です。

## TS 5.6でのBuiltinIteratorの導入

ところで、`Set`の`values`のような組み込みの関数から得られたイテレータには、どんな型が付いているのでしょうか。

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

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

実は、これはTS 5.6で大きく変わったところです。

TS 5.5までは、`values`の型は`IterableIterator<number>`でした。単に`number`のイテレータであることに加えて、for-of文などで扱えるように`Iterable`であることを表しています。

一方、TS 5.6では`BuiltinIterator<number>`になります。

お察しのように、これはTS 5.6で新しく導入された型であり、イテレータが`filter`などのメソッドを持っているのは`BuiltinIterator`のメソッドとして定義されているからです。

つまり、TS 5.6では、`Set`の`values`メソッドなど、組み込み関数で`IterableIterator`を返すもの型定義はことごとく`BuiltinIterator`に書き換えられたというわけです。

### Iterator helpersはランタイムではどこに定義されているのか

ここからややこしい話に入っていきます。実行時において（つまりJavaScript仕様において）、iterator helpersのメソッドはどこに定義されているのでしょうか。その答えは、`Iterator.prototype`です。実は、組み込みの関数が返すイテレータは`Iterator`のインスタンスなのです。

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

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

console.log(values instanceof Iterator); // true
console.log(values.filter === Iterator.prototype.filter); // true
```

つまり、`Iterator`というクラスがグローバルに定義されていることになります。これはiterator helpersの導入と同時に追加されました。

### 名前のミスマッチとその結果

ここで、違和感に気づいた方もいるでしょう。TypeScriptにおいては、「`X`というクラスのインスタンスは`X`型を持つ」というのが基本です。

```ts
const s = new Set([1, 2, 3]); // sはSet<number>型
const d = new Date(); // dはDate型

class MyClass {}

const obj = new MyClass(); // objはMyClass型
```

しかし、`Iterator`においてはそれは満たされていません。`Iterator`というクラスのインスタンスは、`Iterator`型ではなく`BuiltinIterator`です。

どうして、このように慣習と違う定義になってしまったのでしょうか。その理由は、[TS 5.6 Betaのリリースブログ](https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#iterator-helper-methods:~:text=unfortunate%20name%20clash)で *unfortunate name clash* （不幸な名前の衝突）と説明されています。

お察しのとおり、TypeScriptには以前から`Iterator`型がすでに存在していたので、破壊的変更を避けるために、新しい概念である「`Iterator`クラスのインスタンス」の型を`Iterator`にすることができなかったのです。そのため、TypeScriptでは「型としての`Iterator`と値（グローバル変数の名前）としての`Iterator`は無関係」というややこしい状態になってしまいました。

TypeScriptにおける`Iterator`型は、これまでもこれからも「イテレータのプロトコルに従う型」の意味です。昔から`next`メソッドさえ持っていればイテレータとしては適格なオブジェクトであり、iterator helpersが導入されてもそれは変わりません。Iterator helpersは、組み込み関数から生み出されるイテレータは、イテレータに最低限必要な`next`メソッドに加えて、追加の便利なメソッドを持っていますよという機能です。

## `strictBuiltinIteratorReturn`

同じくTS 5.6 Betaで導入された `strictBuiltinIteratorReturn` という新しいコンパイラオプションについても解説します。名前がstrictで始まっていることから想像されるように、このオプションはstrictファミリーの一つです。そのため、`strict`が有効にされていると自動的にこれも有効になります。

このオプションを理解するためには、まず`BuiltinIterator`を返す関数の型定義を見てみましょう。先ほどから使ってきた`Set`のvaluesメソッドは、こんな定義になっています。

```ts
interface Set<T> {
    // ...
    /**
     * Returns an iterable of values in the set.
     */
    values(): BuiltinIterator<T, BuiltinIteratorReturn>;
}
```

よく見ると`BuiltinIterator`の型引数が2つあります。1つ目はイテレータが出力する要素ですが、2つ目はなんでしょうか。

実は、`BuiltinIterator`は型引数が3つあります。それどころか、元々ただの`Iterator`にも型引数が3つありました。

```ts
interface Iterator<T, TReturn = any, TNext = any> {
  // ...
}
interface BuiltinIterator<T, TReturn = any, TNext = any> {
  // ...
}
```

これによると、2つ目の型引数は`TReturn`です。この型引数が使われるのは、`next`の返り値です。`next`の返り値はオブジェクトで、`done`プロパティと`value`プロパティを持ちます。`done`が`false`のときは`value`の型は`T`ですが、`done`が`true`のときは`value`の型は`TReturn`なのです。

```ts
interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface Iterator<T, TReturn = any, TNext = any> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...[value]: [] | [TNext]): IteratorResult<T, TReturn>;
    // ...
}
```

つまり、イテレータが全部の要素を出し終わって終了するときに、終了時の値として最後に与えられるのが`TReturn`なのです。

しかし、この「終了時の値」は仕様として存在しているものの、全然活用されておらず事実上闇に葬られています。例えば、for-of文にイテレータを渡したときは「終了時の値」は無視されます。

ここでの問題は、型引数`TReturn`のデフォルトが`any`であることです。よって、例えば`BuiltinIterator<number>`と書いたら実は`BuiltinIterator<number, any, any>`と同じ意味になるということです。普段使われないとはいえ、`any`が紛れるのはよくありません。

`Iterator`の時点ですでにデフォルトが`any`になっていたのですが、この定義に依存しているTSコードが存在することを考えると、デフォルトを変えるのは後方互換性の観点から困難です。

:::note
**唐突な宣伝**: 筆者が開発している[better-typescript-lib](https://github.com/uhyo/better-typescript-lib)は後方互換性を必要としない人向けの標準ライブラリであり、`TReturn`のデフォルト値は`unknown`に修正されています。
:::

しかし、新しく導入された`BuiltinIterator`であれば`any`という負の遺産を引き継がなくてもいいのではないか？　というアイデアで導入されたのが`BuiltinIteratorReturn`です。もう一度`Set`の`values`の定義を見てみましょう。

```ts
interface Set<T> {
    // ...
    /**
     * Returns an iterable of values in the set.
     */
    values(): BuiltinIterator<T, BuiltinIteratorReturn>;
}
```

`TReturn`が`BuiltinIteratorReturn`となっていますが、実はこの`BuiltinIteratorReturn`という型は、`strictBuiltinIteratorReturn`が有効のときは`undefined`になり、無効のときは`any`になるという特殊な型となっています。

これにより、このオプションが有効な状況では`Set`の`values`のようなイテレータを返す組み込み関数の返り値は`BuiltinIterator<number, undefined>` のような型となり、`any`が削減されています。

もし型定義を見ていて`BuiltinIteratorReturn`が出てきたら、このことを思い出しましょう。

## TypeScriptでiterator helpersを扱うにあたって注意すること

命名のややこしさはあるものの、iterator helpersに関する型定義はうまく書かれています。我々は特に苦労することなく、iterator helpersの便利さを享受することができるでしょう。

しかし、型注釈を明記するときはすこし注意しましょう。イテレータを変数に入れる際、本当は`BuiltinIterator`なのに変数の型を`Iterator`にしてしまった場合は、iterator helpersのメソッドが使えなくなってしまいます。

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

const values: Iterator<number> = nums.values();

const evenValues = values.filter(n => n % 2 === 0);
//                        ^^^^^^ 型エラーになってしまう！
```

この場合、`values`の型注釈を消せば型が`BuiltinIterator<number, undefined, any>` （`strict`有効の場合）となり`filter`が使えるようになります。

このように、昔に書かれた型注釈がiterator helpersの使用を妨げてしまうこともあるので注意しましょう。また、新たに型注釈を書くときは、`Iterator`を使うべきなのか`BuiltinIterator`なのか、あるいは`Iterable`などほかの型が適しているのかを考える必要があります。この記事には出てきませんでしたが`Generator`などもあります。

また、`BuiltinIterator`についても、型定義では先ほど触れた`BuiltinIteratorReturn`が使われているので、それを忘れて`BuiltinIterator<number>`のように書くのは多少危険です（正直大差ないとはいえ）。

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

const values: BuiltinIterator<number> = nums.values();
// ↑ 型推論に任せたら BuiltinIterator<number, undefined, any> なのに、
// 型注釈を書いたら BuiltinIterator<number, any, any> になってしまった！
```

:::note
この点については筆者としては不満をかんじたので、issueで聞いてみています。
https://github.com/microsoft/TypeScript/issues/59444
:::

## 余談

TS 5.6 Betaでは、`BuiltinIterator`に加えて`BuiltinAsyncIterator`の定義も追加されています。今のところasyncイテレータのメソッドは用意されていないのでこの型が存在する意味は特にないのですが、[将来的にasyncイテレータにもメソッドが追加される](https://github.com/tc39/proposal-async-iterator-helpers)ときに備えて`BuiltinAsyncIterator`も用意されたようです。

ちなみに、最初は`AsyncBuiltinIterator`という名前になりそうだったのですが、[petamorikenさんの活躍](https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1713#issuecomment-2241803236)により、より適した名前と思われる`BuiltinAsyncIterator`に直されました。

## まとめ

この記事では、TS 5.6で追加されたiterator helpersについて、型定義周りの事情を中心に解説しました。特に、iterator helpersのメソッドたちの実際の定義は新たに導入された`BuiltinIterator`型に住むことになるので、この型を知っておくことは重要です。