TypeScriptの識別子付きユニオン型による安全な分岐処理

TypeScript Frontend TypeSystem
結論

識別子で switch 分岐し、default 内に const _check: never = value を書いて実装漏れを防ぎます。

// TypeScript公式仕様:Discriminated Unions と Exhaustiveness Checking
type NetworkLoadingState = { state: "loading" };
type NetworkFailedState = { state: "failed"; code: number };
type NetworkSuccessState = { state: "success"; response: string };

// 識別子 (state) を共有するユニオン型
type NetworkState = NetworkLoadingState | NetworkFailedState | NetworkSuccessState;

function handleState(state: NetworkState): string {
  switch (state.state) {
    case "loading":
      return "読み込み中...";
    case "failed":
      return `エラーコード: ${state.code}`;
    case "success":
      return `完了: ${state.response}`;
    default:
      // ⭕ 網羅性チェック(新メンバー追加時にコンパイルエラー化)
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck;
  }
}

TypeScript公式仕様:Narrowing(型絞り込み)メカニズム

識別子付きユニオン型(Discriminated Unions)は、すべてのオブジェクトメンバーが 共通のプロパティ(この例では state をリテラル型で保持している型構造です。

switchif 文でその識別子をチェックすると、TypeScriptのフロー解析エンジン(Control Flow Analysis)がブロック内の型を自動的に特定オブジェクト型へ絞り込み(Narrowing)します。


実際に起こる事故:新ステータス追加時の処理モレ障害

将来仕様が変更され、NetworkState に新しい状態 | { state: "canceled" } が追加されたとします。

もし default: ブロックで never による網羅性チェックを行っていない場合、コンパイルはそのまま通過し、本番環境で canceled 状態が来た際に 何も処理されず undefined が返却されてUIがフリーズする障害 が発生します。

const _exhaustiveCheck: never = state; を書いておくことで、新状態の追加時にコンパイラが以下のエラーを吐いてビルドを即座にブロックします:

Type '{ state: "canceled"; }' is not assignable to type 'never'.

実装手順

  1. ユニオン内の全オブジェクトに共通する文字列リテラルプロパティ(typestate)を持たせる
  2. switch (obj.type) を使って条件分岐コードを記述する
  3. default: ブロック内に const _exhaustiveCheck: never = obj; を配置し、将来の追加漏れを型レベルで監視する