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