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"'.
解説
type Sample = "A" | "B";
で、"A"
または"B"
のいずれかの文字列リテラルのみを許容するユニオン型Sample
を定義します。switch (sample)
文で、sample
の値に応じて処理を分岐させます。default
句では、case "A"
とcase "B"
の両方が処理された後、sample
の型は理論上「あり得ない」状態になります。もしsample
が"A"
でも"B"
でもない値であれば、このdefault
句に到達します。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
文に書かれていない場合に、早期にエラーを検知できるようになります。
参考
- Software Design 編集部, 『Software Design (ソフトウェアデザイン) 2024年05月号』, 株式会社技術評論社 (p.102-P123)
- TypeScript Deep Dive 日本語版: never