TypeScript 5.6で強化されたIterator操作:IteratorObjectとBuiltinIteratorReturn

typescript logo プログラミング


TypeScriptの5.6リリースでは、新しいインターフェースIteratorObjectが導入されました。

この新機能は TC39のプロポーザル「Iterator Helpers」と密接に関連しており、
その背景には「名前の衝突」を回避するという重要な課題があります。

この記事は、TypeScriptにおけるIteratorObjectの概要と、その導入背景についての話となります。


TypeScriptにおける従来のIteratorとIterable

まずは、TypeScriptでのIteratorとIterableの違いについて簡単におさらいしておきます。

TypeScriptにはこれまで、IteratorIterableという2つの重要なインターフェースが存在していました。

Iterator:
– イテレータは、next() メソッドを持ち、順に値を返すオブジェクトです。例えば、配列やSetのvalues()メソッドが返すものがイテレータです。next() を呼び出すと、その都度次の値が返され、すべての値が返されると done: true が返されます。

Iterable:
– 反対に、イテラブルは、イテレータを生成するオブジェクトです。例えば、配列やSetなどは [Symbol.iterator] メソッドを持ち、これがイテレータを返します。for-ofループで使えるオブジェクトは、すべてこのIterableに該当します。


これらは、JavaScriptの反復処理プロトコルに基づいており、TypeScriptではそのインターフェースとして実装されてきました。

反復処理プロトコル - JavaScript | MDN
反復処理プロトコルは、新しい組み込みオブジェクトや構文ではなくプロトコルです。これらのプロトコルは以下のような単純な約束事によって、すべてのオブジェクトで実装することができます。

プロポーザル「Iterator Helpers」


そんな中で、JavaScriptにおいて提案されているのが、Iterator Helpersです。

GitHub - tc39/proposal-iterator-helpers: Methods for working with iterators in ECMAScript
Methods for working with iterators in ECMAScript. Contribute to tc39/proposal-iterator-helpers development by creating a...

このプロポーザルは、Iteratorにいくつかの便利なメソッド(例えば map()filter()take() など)を追加することを目的としており、特にジェネレータや他のイテレータをより直感的かつ効率的に扱えるようにすることを狙っています。

プロポーザルのステータスは現在stage3で、chromeやedgeに実装されています。

mapfilterなどのメソッドは、今までArrayにしか実装されておらず、他の反復可能オブジェクトには実装されていないのでした)


例えば、次のようにジェネレータを使用したコードが map() メソッドで簡単に処理できるようになります。

JavaScript
function* positiveIntegers() {
    let i = 1;
    while (true) {
        yield i;
        i++;
    }
}

const evenNumbers = positiveIntegers().map(x => x * 2);
for (const value of evenNumbers.take(5)) {
    console.log(value);
}

このコードでは、positiveIntegers という無限のジェネレータから偶数を取り出し、最初の5つだけを出力しています。
このように、map()take() といったメソッドが、従来の配列操作だけでなくイテレータ上でも直接使えるようになったのです。


TypeScriptとの名前の衝突

Iterator Helpers提案により、JavaScriptランタイムにIteratorという新しいオブジェクトが導入されました。
これにより、実際のJavaScriptコード中で Iterator.prototype に新しいメソッドが追加され、直接利用できるようになりました。

しかし、ここで問題が発生します。

TypeScriptではすでにIteratorという名前の型が存在しており、これは主に型チェック用に使用されていました。
これにより、JavaScriptのランタイム側で導入された新しいIteratorオブジェクトと、TypeScript内の既存のIterator型の間で名前の衝突が発生してしまったのです。


IteratorObject

IteratorObjectの登場

この名前の衝突を解決するために、TypeScript v5.6では新たに IteratorObject というインターフェースが導入されました。
このIteratorObjectは上で説明した、JavaScriptのランタイムで提供される新しいIteratorオブジェクトを正確に型付けするために使われます。

たとえば、SetやMapの values() メソッドが返すイテレータは、これまで IterableIterator<T> として型付けされていました。しかし、TypeScript 5.6では次のように変更されています。

TypeScript
interface SetIterator<T> extends IteratorObject<T, BuiltinIteratorReturn, unknown> {
    [Symbol.iterator](): SetIterator<T>;
}


この定義により、SetやMapが返すイテレータはIteratorObjectとして扱われるようになり、これによってJavaScriptランタイムとTypeScriptの型システム間での名前の衝突が解消されました。

IteratorObjectの詳細

IteratorObjectの定義は、以下のようになっています。

TypeScript
interface IteratorObject<T, TReturn = unknown, TNext = unknown> extends Iterator<T, TReturn, TNext> {
    [Symbol.iterator](): IteratorObject<T, TReturn, TNext>;
}

IteratorObjectは、 Iterator<T, TReturn, TNext> を拡張したものであり、 [Symbol.iterator]() メソッドを持つことで、イテレータ自体がイテラブル(反復可能)であることを保証します。この定義を詳細に見てみます。

定義の詳細

型引数T

Tは、イテレータが生成する要素の型を指定するための型引数です。
例えば、IteratorObject<number>とすると、そのイテレータはnumber型の値を順に返すことを表します。配列やセットなどのコレクションを操作する際には、通常、この型引数にはそのコレクションの要素の型を指定します。

型引数TReturn

TReturnは、イテレータが終了したときに返す値の型を指定します。

例えば、ジェネレータ関数を使ったIteratorは、すべての要素を返し終えた後に特定の「終了値」を返します(done: true とともに)。TReturnはその終了値の型を定義します。
デフォルトではunknownとなっており、特に指定しない限り、終了時の値はunknown型として扱われます。

型引数TNext

TNextは、next()メソッドに渡される値の型を指定します。
通常のイテレータでは、次に何の値を生成するかに影響を与えるために、next()メソッドに値を渡すことができます。TNextはその引数の型を表します。

例えば、next()メソッドに数値やオブジェクトを渡して、その値をもとに次の値を生成するようなイテレータでは、TNextにその引数の型が指定されます。
これが省略された場合、unknownがデフォルト値となります。

[Symbol.iterator]()メソッド

IteratorObjectインターフェースには [Symbol.iterator]() メソッドが定義されています。
これは、イテレータ自身を反復可能オブジェクト(Iterable)にするためのものです。
このメソッドがあることで、IteratorObject自体がfor-ofループなどで使えるようになります。

例えば、次のように書くと、このIteratorObjectをfor-ofでループすることができます。

TypeScript
const iterator: IteratorObject<number> = someIteratorFunction();
for (const value of iterator) {
    console.log(value);
}

型引数の組み合わせによる柔軟性

IteratorObjectでは、これらの型引数がデフォルトでunknownに設定されていますが、これにより多様なケースで型安全なIterator操作が可能になります。例えば、以下のような例が考えられます。

1. 単純なIterator

例えば、単に数値を順に返すイテレータの場合、型引数は次のように使われます。

TypeScript
const iterator: IteratorObject<number> = createNumberIterator();
2. 終了時の値を指定するIterator

特定の終了値を返すイテレータでは、 TReturn を指定してその型を明示します。

TypeScript
const iterator: IteratorObject<number, string> = createNumberIteratorWithReturn();
next()に引数を渡すイテレータ

TNextを指定すると、next()に渡す値の型も制御できます。

TypeScript
const iterator: IteratorObject<number, void, boolean> = createBooleanControlledIterator();
iterator.next(true); // `true`を渡して次の値を制御

BuiltinIteratorReturn と strictBuiltinIteratorReturn


さらにTypeScript v5.6では、BuiltinIteratorReturnという新しい型が導入されました。
これは、配列やSetなどの組み込みイテレータの終了時(done: true の状態に達したとき)に返される値を型安全に扱うためのものです。

また、これに伴い –strictBuiltinIteratorReturnというコンパイラオプションも追加されています。

BuiltinIteratorReturn と strictBuiltinIteratorReturn は、一緒に使うことで効力を発揮します。

BuiltinIteratorReturnの背景

イテレータには、next() メソッドがあり、その戻り値には2つのケースがあります。

1. まだ要素がある場合: done: false とともに次の値を返します。
2. 要素がすべて消費された場合: done: true とともに終了時の値を返します。

従来、この終了時の値(done: trueのときに返される値)は、デフォルトでany型が使われていました。これは、型安全性の観点から好ましくありません。
特に、終了時に返される値がundefinedやvoidの場合、any型だと予期しない型の値が返されてもエラーになりません。

これを改善するために、BuiltinIteratorReturn が導入され、TReturn 型引数としてこの型を指定することで、イテレータの終了時に返される値を安全に扱えるようになりました。

strictBuiltinIteratorReturn オプションとは?


strictBuiltinIteratorReturn を有効にすると、BuiltinIteratorReturn がデフォルトで undefined として扱われるようになります。
このオプションは、イテレータの終了時に常に型安全な値を要求するためのものです。有効にすることで、終了時の値が誤って使用された場合にエラーが発生するようになります。

具体例

1. BuiltinIteratorReturn / strictBuiltinIteratorReturn を使わない場合

BuiltinIteratorReturn を使わず、イテレータの終了時に誤った操作をしている例を見てみます。

TypeScript
function* createIterator() {
    yield "first";
    yield "second";
    return 10;  // 終了時の値として10を返す
}

const iter = createIterator();
let result = iter.next();

// 終了時は10を返すので本来はtoUpperCaseメソッドはエラーになるが、
// any型として処理されるためコンパイル時にエラーが出ない
console.log(result.value.toUpperCase());

終了値が number が返されますが、それはany型として返されるため、toUpperCaseがエラーになりません。すなわち、型安全なチェックが行われていない状況です。

2. BuiltinIteratorReturn / strictBuiltinIteratorReturn を使う場合
TypeScript
// tsconfig.json
{
  "compilerOptions": {
    "strictBuiltinIteratorReturn": true
  }
}
TypeScript
function* createIterator(): Iterator<string, BuiltinIteratorReturn> {
    yield "first";
    yield "second";
    return 10;  // 終了時の値として10を返す
}

const iter = createIterator();
let result = iter.next();

if (!result.done) {
    console.log(result.value.toUpperCase());  // 正常に動作
} else {
    console.log(result.value);  // `undefined`または特定の終了値を期待
}


strictBuiltinIteratorReturn オプションを有効にし、BuiltinIteratorReturn をIteratorの終了時の返り値の型として指定することで、BuiltinIteratorReturnはundefinedとして扱われ、イテレータの終了時に返される値が型安全に制御されます。

このオプションが有効になると、終了時にany型が使われることを防ぎ、実行時に予期しないエラーが発生するリスクを減らすことができます。

なおv5.6のリリース時点では、strictBuiltinIteratorReturnオプションを有効にしない状態だと、
BuiltinIteratorReturnはany型となります。つまり今までの状態と同じということです。
これは、strictBuiltinIteratorReturnオプションのON/OFFで厳密な型チェックを切り替えられるようにすることで、広報互換性を保った形で既存のコードの移行をスムーズに行えるようにするためのようです。


まとめ

IteratorObjectの導入がもたらすメリット

IteratorObjectの導入により、TypeScriptは次のようなメリットを享受しています。

1. 名前の衝突を回避

JavaScriptランタイムのIteratorオブジェクトと、TypeScript内で既に存在していたIterator型との間の競合を回避できます。

2. ネイティブメソッドのサポート

IteratorObjectは、JavaScriptのネイティブイテレータメソッド(mapfiltertakeなど)を直接サポートします。
これにより、従来のArrayメソッドでできた操作を、イテレータ上でも同じように適用できます。

3. 型安全なコードの作成:

IteratorObjectは、ジェネレータやビルトインのイテレータを型安全に扱うことができるように設計されています。これにより、開発者は意図しない型の不整合やエラーを回避でき、より堅牢なコードを記述できます。

BuiltinIteratorReturn / strictBuiltinIteratorReturnの導入がもたらすメリット


BuiltinIteratorReturnとstrictBuiltinIteratorReturnは、イテレータの終了時に返される値の型安全性を強化するために導入されました。
特に、ビルトインのイテレータが終了した際に、予期しない値が返されるのを防ぎ、開発者が意図せずに型ミスを犯すのを防止する役割を果たします。


(余談ですが、JavaScriptでは AsyncIterator というのものはまだランライムに存在しません。ですがいずれ実装される時に備えて、 iterator helpersの非同期バージョンが既にプロポーザルとして存在しています。↓)

GitHub - tc39/proposal-async-iterator-helpers: Methods for working with async iterators in ECMAScript
Methods for working with async iterators in ECMAScript - tc39/proposal-async-iterator-helpers

コメント

タイトルとURLをコピーしました