TypeScriptにおけるジェネレーター関数は、反復処理や非同期処理の制御において非常に強力なツールとなります。
JavaScriptで導入されたこの機能は、関数の実行を途中で停止し、後から再開できるというユニークな特性を持っています。
TypeScriptでは、これらのジェネレーターに対して厳格な型定義を行うことができ、実行時のエラーを未然に防ぐことが可能です。
本記事では、ジェネレーター関数の基本的な型定義から、実務で役立つ応用的なテクニックまでを詳しく紹介します。
最新のTypeScript環境において、安全にジェネレーターを使いこなすための知識を深めていきましょう。
ジェネレーター関数の基本構造と型推論
ジェネレーター関数を定義するには、function*という構文を使用します。
この関数は呼び出されると、通常の関数のように即座に実行されるのではなく、ジェネレーターオブジェクトを返します。
TypeScriptは、この関数が何をyieldし、何をreturnするかに基づいて、自動的に型を推論する能力を持っています。
まずは、最もシンプルな数値のシーケンスを生成する例を見てみましょう。
function* simpleNumberGenerator() {
yield 1;
yield 2;
yield 3;
return "完了";
}
const gen = simpleNumberGenerator();
for (const value of gen) {
console.log(value); // 1, 2, 3 が順に出力される
}
1
2
3
このコードにおいて、TypeScriptは変数genの型をGenerator<number, string, unknown>と推論します。
ジェネレーターの型は3つの型引数を持ち、それぞれが重要な役割を担っています。
Generator型の3つの型引数
TypeScriptの標準ライブラリで定義されているGenerator型は、次のようなシグネチャを持っています。
Generator<T, TReturn, TNext>という形式で定義されており、それぞれの意味は以下の通りです。
| 型引数 | 説明 |
|---|---|
| T | yieldキーワードによって返される値の型(産出型)。 |
| TReturn | returnキーワードによって返される値の型(返却型)。 |
| TNext | next()メソッドの引数として渡される値の型(注入型)。 |
実務においては、yieldされる値の型であるTを最も頻繁に利用します。
一方で、TReturnやTNextは、特定の高度なパターンを実装する場合に意識する必要があります。
明示的な型定義による安全性の向上
型推論に頼ることもできますが、関数のシグネチャを明確にするために、戻り値を明示的に記述することが推奨されます。
特に、ジェネレーターが複雑なオブジェクトを返す場合や、複数の型を混在させる場合には、明示的な定義が欠かせません。
// 明示的に型を指定したジェネレーター関数
function* prioritizedTaskGenerator(): Generator<string, number, unknown> {
yield "データの読み込み";
yield "バリデーションの実行";
yield "データベースへの保存";
return 200; // ステータスコードを返す
}
const taskGen = prioritizedTaskGenerator();
このように記述することで、意図しない型の値をyieldしようとした際にコンパイルエラーを発生させることができます。
例えば、上記のコードでyield 100;と記述すると、TypeScriptは「string型にnumber型は割り当てられない」という警告を出してくれます。
TNextを活用した双方向のデータ受け渡し
ジェネレーターの強力な特徴の一つは、外部から値を注入できることです。
next(value)を呼び出すことで、ジェネレーター内のyield式の戻り値としてその値を受け取ることができます。
このTNextの型を適切に定義することで、安全なメッセージパッシングが可能になります。
function* interactionGenerator(): Generator<string, void, number> {
// yieldの戻り値を受け取る。この型がTNext(number型)になる
const response = yield "数値を入力してください";
console.log(`受け取った数値の2倍は: ${response * 2}`);
}
const it = interactionGenerator();
it.next(); // 最初のyieldまで実行
it.next(42); // responseに42が代入される
受け取った数値の2倍は: 84
ここで注意が必要なのは、最初のnext()呼び出しには引数を渡すことができないという点です。
TypeScript 5.x以降では、このTNextの扱いがより厳密になり、unknown型として定義して実行時に型ガードを行うパターンも増えています。
非同期ジェネレーターとAsyncGenerator型
2026年のモダンなWeb開発において、ストリーミングデータやAPIのページネーション処理にはAsyncGeneratorが多用されます。
非同期ジェネレーターは、async function*という構文で定義され、戻り値の型はAsyncGenerator<T, TReturn, TNext>となります。
interface User {
id: number;
name: string;
}
async function* fetchUsers(pageLimit: number): AsyncGenerator<User, void, unknown> {
for (let i = 1; i <= pageLimit; i++) {
const response = await fetch(`https://api.example.com/users?page=${i}`);
const data: User[] = await response.json();
for (const user of data) {
yield user; // ユーザーを一人ずつ産出
}
}
}
async function processUsers() {
for await (const user of fetchUsers(3)) {
console.log(`処理中のユーザー: ${user.name}`);
}
}
AsyncGeneratorを使用することで、大量のデータを一度にメモリに読み込むことなく、効率的に処理をストリーム化できます。
この際、for await...of構文を使用することで、非同期に産出される値を直感的に扱うことができます。
ジェネレーターにおける型安全性の実践テクニック
IterableIteratorとの使い分け
TypeScriptにはGenerator型に似た型として、IterableIteratorが存在します。
Generator型は、next()、return()、throw()のすべてのメソッドを持つことを保証しますが、IterableIteratorは反復可能であることのみを保証します。
通常、独自のジェネレーター関数を作成する場合は、より詳細な型定義が可能なGenerator型を使用するのがベストプラクティスです。
一方で、既存の反復可能なオブジェクトをラップして返すだけの関数であれば、IterableIteratorで十分な場合もあります。
無限シーケンスの定義
ジェネレーターは、無限に続くシーケンスを生成するのにも適しています。
この場合、TReturnの型をneverまたはvoidとして定義することが一般的です。
function* idGenerator(): Generator<number, never, unknown> {
let id = 1;
while (true) {
yield id++;
}
}
const ids = idGenerator();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
neverを指定することで、この関数が正常に終了(return)することがないことを開発者に明示できます。
yield* を使ったジェネレーターの委譲
一つのジェネレーターから別のジェネレーターへ処理を委譲する場合、yield*を使用します。
このとき、TypeScriptは委譲先のジェネレーターの型を正しく解釈し、全体の型整合性をチェックします。
function* innerGenerator(): Generator<string, void, unknown> {
yield "A";
yield "B";
}
function* outerGenerator(): Generator<string, void, unknown> {
yield "開始";
yield* innerGenerator(); // 別のジェネレーターに委譲
yield "終了";
}
委譲先のジェネレーターがyieldする型は、委譲元のGenerator<T>のTと互換性がなければなりません。
ジェネレーターとエラーハンドリング
ジェネレーター内部で発生したエラーや、外部から注入されたエラーを型安全に処理することも重要です。
try...catchブロックを使用することで、ジェネレーターの実行中に発生した例外を捕捉できます。
function* exceptionHandlingGenerator(): Generator<string, string, unknown> {
try {
yield "実行中...";
throw new Error("内部エラー発生");
} catch (error) {
return `エラーを捕捉しました: ${error instanceof Error ? error.message : "不明なエラー"}`;
}
}
const errorGen = exceptionHandlingGenerator();
console.log(errorGen.next().value);
console.log(errorGen.next().value);
このように、エラー発生時の戻り値をTReturn型に含めて定義することで、安全な事後処理が可能になります。
まとめ
TypeScriptにおけるジェネレーター関数の型定義は、コードの予測可能性と安全性を飛躍的に向上させます。
Generator<T, TReturn, TNext>という3つの型引数を理解し、適切に使い分けることが、高度な開発への第一歩です。
特に大規模なアプリケーションにおいては、非同期処理やストリーミング処理をジェネレーターでカプセル化することで、見通しの良い設計が可能になります。
型推論を最大限に活用しつつ、必要な箇所では明示的な型定義を行うことで、バグの少ない堅牢なプログラムを構築してください。
ジェネレーターの特性を活かし、TypeScriptの型システムの恩恵を十分に享受しましょう。
