TypeScriptのnever型でswitch文の網羅性チェック実装

TypeScriptでユニオン型 (Union Type) に対して switch 文を使用する場合、すべてのケースを網羅していることを保証することが重要です。ユニオン型に新しい型が追加された際に、switch 文の処理が更新されないと、予期せぬバグにつながる可能性があります。

この網羅性をコンパイル時にチェックする方法として、switch 文の default 句に never 型を代入するテクニックがあります。

never 型とは

never 型は、TypeScriptの特別な型で、「決して発生しない値」を表します。これは、関数が何も返さない(無限ループに陥る、常に例外をスローするなど)場合や、到達不能なコードパスの型として使用されます。

より詳しく: TypeScript Deep Dive: never

簡単に言えば、never 型は「あり得ない」ことを表す型です。

never 型を用いた網羅性チェックの仕組み

このテクニックのサンプル実装は以下のようになります。

type Sample = "A" | "B"; // ユニオン型を定義

function testFunc(sample: Sample): void {
  switch (sample) {
    case "A":
      console.log("Case A:", sample);
      break;
    case "B":
      console.log("Case B:", sample);
      break;
    default:
      // ここで網羅性をチェック
      // 'sample' の型が 'never' でなければコンパイルエラーになる
      const _: never = sample;
      // もしここに到達する可能性があれば、TypeScriptはエラーを報告する
  }
}

// 実行例
testFunc("A"); // "Case A: A"
testFunc("B"); // "Case B: B"
// testFunc("C"); // コンパイルエラー: Argument of type '"C"' is not assignable to parameter of type '"A" | "B"'.

解説

  1. type Sample = "A" | "B"; で、"A" または "B" のいずれかの文字列リテラルのみを許容するユニオン型 Sample を定義します。
  2. switch (sample) 文で、sample の値に応じて処理を分岐させます。
  3. default 句では、case "A"case "B" の両方が処理された後、sample の型は理論上「あり得ない」状態になります。もし sample"A" でも "B" でもない値であれば、この default 句に到達します。
  4. const _: never = sample; の行がポイントです。TypeScriptの型検査器は、この時点で sample の型が never であることを期待します。
    • もし switch 文の case 句がユニオン型のすべてのパターンを網羅していれば、default 句に到達する sample の値は存在しないため、sample の型は never と推論され、コンパイルは成功します。
    • 例えば、ユニオン型 Sample| "C" を追加したにもかかわらず、case "C": を追加し忘れた場合、default 句に到達する sample の型は never ではなく "C" と推論されます。このとき、"C" 型の値を never 型の変数に代入しようとすることになるため、TypeScriptはコンパイルエラーを報告します。

このテクニックを用いることで、Union 型のすべてのパターンに対して漏れなく処理が書かれていることをコンパイル時に保証できます。これにより、将来 Union 型に新しい型が追加されたときに、それに対応する処理が switch 文に書かれていない場合に、早期にエラーを検知できるようになります。

参考