分岐漏れをコンパイル時につぶす
開発環境
- typescript 5.9.3
前提
TypeScript を使っていると、型安全性を高めたい場面によく遭遇します。
特に 列挙型の分岐 (switch / if) では、ケースの抜け漏れがあると意図しない挙動につながります。
たとえば Enum を扱うコードで
enum Direction {
Up,
Down,
Left,
Right,
}
const move = (dir: Direction) => {
switch (dir) {
case Direction.Up:
return "↑";
case Direction.Down:
return "↓";
// Left, Right を処理し忘れてしまった...
}
};コンパイルエラーにはならず、一部のケースが未対応のままコードが通ってしまいます。
これでは 分岐漏れ を検知できません。
この問題を コンパイル時に検知したい というニーズが出てきます。
本題
分岐漏れチェックの基本パターン
TypeScript で分岐漏れを確実に検出する代表的な方法は、未到達コードの型を never にすることです。
const assertNever = (x: never): never => {
throw new Error("Unexpected value: " + x);
};
const move = (dir: Direction) => {
switch (dir) {
case Direction.Up:
return "↑";
case Direction.Down:
return "↓";
case Direction.Left:
return "←";
case Direction.Right:
return "→";
default:
return assertNever(dir); // ここで漏れをチェック
}
};dir が未処理のケースを含んでいるとコンパイルエラーになります。
ただしこの方法は、毎回 assertNever を用意する必要があり、やや冗長に感じることもあります。
satisfies never を使ったよりシンプルな方法
TypeScript 4.9 以降で使える satisfies を活用すると、より直感的に「ここには到達しないはず」ということを表現できます。
const move = (dir: Direction) => {
switch (dir) {
case Direction.Up:
return '↑';
case Direction.Down:
return '↓';
case Direction.Left:
return '←';
case Direction.Right:
return '→';
default:
return dir satisfies never; // ここで漏れをチェック
}
};オブジェクト網羅チェックにも応用できる
switch だけでなく、Direction のマッピングをオブジェクトで表現する場合も同様です。
const directionLabel = {
[Direction.Up]: "↑",
[Direction.Down]: "↓",
[Direction.Left]: "←",
[Direction.Right]: "→",
} satisfies Record<Direction, string>;いずれかを書き忘れると、その場で型エラーになります。
as Record<...> と違い、
satisfies Record<...> は 型を壊さずに検証だけを行う のがポイントです。
なぜ satisfies never が良いのか
- 追加の関数を用意する必要がない
- 「ここは到達不能であるべき」という意図が明確
- 型アサーション (as) と違って安全性を壊さない
- enum / union が増えた瞬間に漏れが検知できる
分岐漏れはレビューでは見落としやすく、実行時バグにもつながります。
それを コンパイル時に確実に潰せる のは TypeScript の強みです。
さいごに
TypeScript を使う最大の価値は、「実行前にバグを消せること」です。
never を活用した exhaustive check は以前からあるテクニックですが、 satisfies never を使うことで、よりシンプルに・意図が伝わる形で書けるようになりました。
型システムに仕事をさせる設計を意識すると、コードは確実に強くなります。 分岐が増えがちなプロジェクトほど、ぜひ取り入れてみてください。