ProgrammingTypeScriptアーキテクト

TypeScriptにおけるパターンマッチングは、識別されるユニオンを通じてどのように実装されますか?型をどのように構造化し、どんな罠があるのでしょうか?

Hintsage AIアシスタントで面接を突破

回答

TypeScriptにおけるパターンマッチングは、"識別されるユニオン"(discriminated unions)を通じて実装されます。ユニオン内の各オブジェクトには、必須の識別子フィールド(通常は文字列、例えば type)が割り当てられ、TypeScriptはこれによってバリアントを区別します。

例:

type Success = { type: 'success'; data: string }; type Failure = { type: 'failure'; error: string }; type Result = Success | Failure; function handleResult(result: Result) { switch (result.type) { case 'success': // result: Success console.log(result.data); break; case 'failure': // result: Failure console.error(result.error); break; } }

switch/caseまたはif文で識別子フィールドを使うと、TypeScriptは型を正確に必要なバリアントに絞り込みます。

主な利点:

  • 厳密な型指定 — 存在しないフィールドにアクセスすることはできません。
  • 網羅性のチェック — すべてのバリアントを処理しなかった場合、時折エラーが発生します(明示的に強制することも可能です)。

ひねりのある質問

識別されるユニオンに新しいバリアントを追加した場合、TypeScriptは新しいバリアントを処理するためにすべてのswitch-caseを更新することを強制しますか?

回答: いいえ、「不可能な」バリアントを明示的に処理する場合のみです。例えば、never関数を使用します:

例:

function assertNever(x: never): never { throw new Error('Unexpected variant: ' + x); } function handle(r: Result) { switch(r.type) { case 'success': /* ... */; break; case 'failure': /* ... */; break; default: return assertNever(r); // 新しい型が現れたらTSがエラーを出します } }

このテーマの細かい知識の欠如による実際のエラーの例。


物語

'pending'という新しいバリアントを持つ型"Result"を拡張した後、アプリケーションのいくつかの場所で古いswitch-caseがこのケースを処理しなかった。結果、いくつかのインターフェースが機能しなくなりました。エラーはリリース後1週間で生産環境でのみ発見されました。


物語

唯一の識別子(typeフィールドが2つの型で重複している)なしで識別されるユニオンを使用しようとした結果、型が"ぼやけ"、TypeScriptが型を正確に絞り込めなくなり、コンパイルエラーなしに存在しないフィールドにアクセスできるようになってしまいました。いくつかの重大なバグがプロダクションに残りました。


物語

プロジェクトでパターンマッチングをいくつかのフィールドに対するif-elseを通じて実装したため、1つの明示的な識別子を使用する代わりに、never関数を使った網羅性のチェックへの移行が難しくなり、コードの可読性が低下しました — switch-caseが正しく機能せず、新しいバリアントが既存のロジックを"壊す"ことになりました。