exhaustiveness-checking について
exhaustiveness-checkingは、never型を使って未処理のユニオン型の変化をコンパイル時に検出するTypeScriptスキルです。タグ付きユニオン型が変わっても、開発者がswitch文の網羅性を保てるよう支援します。
主な機能
- 01never型による網羅性チェック
- 02switchのdefaultでassertUnreachableを使用
- 03未処理ケースをコンパイル時に検出
- 04明示的な戻り値型で網羅性を強制
- 05意図的な部分処理の判断
活用シーン
- 01タグ付きユニオン型の全変化を処理
- 02ユニオン型へのケース追加後にswitchを更新
- 03TypeScriptコードでの暗黙の処理漏れを防止
- 04選択したケースだけを意図的に処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill exhaustiveness-checkingインストール後、Claude Code で/exhaustiveness-checkingを入力して読み込まれたか確認します。
詳細
網羅性チェックに Never 型を使用する
概要
never を使って、ユニオンのすべてのケースが処理されていることを確認します。
ユニオン型に新しいバリアントを追加すると、更新が必要なすべての switch 文を TypeScript が自動的に検出できます。これにより、対応漏れによるエラーをコンパイル時に捕捉できます。
このスキルを使う場面
- タグ付きユニオンのすべてのケースを処理するとき
- 判別可能なユニオンに新しいバリアントを追加するとき
- すべてのケースを処理する必要がある switch 文を書くとき
- ケースの見落としをコンパイル時エラーにしたいとき
絶対に守るルール
ALWAYS add exhaustiveness checking to switch statements on union types.
覚えておくこと:
- すべてのケースを網羅した後の型は
neverになります never以外の型はneverに代入できません- ケースが不足していると型エラーになります
- これにより、対応漏れによるエラーを捕捉できます
検出:ケースの見落としの問題
網羅性チェックがなければ、新しいユニオンのバリアントは気づかれないまま無視されます。
type Shape = Box | Circle | Line;
function drawShape(shape: Shape, ctx: CanvasRenderingContext2D) {
switch (shape.type) {
case 'box':
ctx.rect(...shape.topLeft, ...shape.size);
break;
case 'circle':
ctx.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
break;
// Forgot 'line' - NO ERROR! Lines silently don't draw.
}
}
網羅性を確認するパターン
assertUnreachable ヘルパー
function assertUnreachable(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
switch 文での使用
function drawShape(shape: Shape, ctx: CanvasRenderingContext2D) {
switch (shape.type) {
case 'box':
ctx.rect(...shape.topLeft, ...shape.size);
break;
case 'circle':
ctx.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
break;
default:
assertUnreachable(shape);
// If we missed a case, shape won't be 'never' and we get a type error!
}
}
新しい型を追加したとき
// Add a new shape type
interface Line {
type: 'line';
start: Coord;
end: Coord;
}
type Shape = Box | Circle | Line; // Added Line
// Now drawShape shows an error:
function drawShape(shape: Shape, ctx: CanvasRenderingContext2D) {
switch (shape.type) {
case 'box': /* ... */ break;
case 'circle': /* ... */ break;
default:
assertUnreachable(shape);
// ~~~~~
// Argument of type 'Line' is not assignable to parameter of type 'never'
}
}
新しいケースを処理して修正します。
function drawShape(shape: Shape, ctx: CanvasRenderingContext2D) {
switch (shape.type) {
case 'box':
ctx.rect(...shape.topLeft, ...shape.size);
break;
case 'circle':
ctx.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
break;
case 'line':
ctx.moveTo(...shape.start);
ctx.lineTo(...shape.end);
break;
default:
assertUnreachable(shape); // Now shape is 'never', no error!
}
}
仕組み
すべてのケースを処理すると、残りの型は never になります。
function processShape(shape: Shape) {
switch (shape.type) {
case 'box': break;
case 'circle': break;
case 'line': break;
default:
shape
// ^? (parameter) shape: never
}
}
ケースを見落とすと、型は never になりません。
function processShape(shape: Shape) {
switch (shape.type) {
case 'box': break;
case 'circle': break;
// (forgot 'line')
default:
shape
// ^? (parameter) shape: Line
}
}
Line は never に代入できないため、型エラーが発生します。
代替方法:戻り値の型による強制
戻り値の型を使って網羅性を強制することもできます。
function getShapeName(shape: Shape): string {
switch (shape.type) {
case 'box':
return 'Box';
case 'circle':
return 'Circle';
// Missing 'line' - TypeScript error!
// Function lacks ending return statement and return type does not include 'undefined'
}
}
この方法が機能するのは、次の条件を満たす場合のみです。
- 関数に明示的な戻り値の型がある
- すべてのケースで戻り値を返す必要がある
strictNullChecksが有効になっている
完全な例
// Types
type Coord = [x: number, y: number];
interface Box {
type: 'box';
topLeft: Coord;
size: Coord;
}
interface Circle {
type: 'circle';
center: Coord;
radius: number;
}
interface Line {
type: 'line';
start: Coord;
end: Coord;
}
type Shape = Box | Circle | Line;
// Helper
function assertUnreachable(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
// Usage - guaranteed to handle all shapes
function getArea(shape: Shape): number {
switch (shape.type) {
case 'box':
return shape.size[0] * shape.size[1];
case 'circle':
return Math.PI * shape.radius ** 2;
case 'line':
return 0; // Lines have no area
default:
return assertUnreachable(shape);
}
}
使用しない場面
一部のケースを意図的に無視したい場合もあります。
function handleCommonShapes(shape: Shape) {
switch (shape.type) {
case 'box':
case 'circle':
// Handle common cases
break;
// Intentionally ignore 'line' - don't add assertUnreachable here
}
}
プレッシャーに対する対応方針
1. 「default ケースで処理できます」
プレッシャー:「default ケースがあるので問題ありません」
回答: 何も処理しない default は、新しいバリアントが追加されたときにバグを隠します。
対応: default に assertUnreachable を使い、ケースの不足を明示します。
2. 「更新するのを忘れません」
プレッシャー:「新しいケースをどこに追加すればよいか分かっています」
回答: 人の記憶はあてになりません。コンパイラーのチェックなら確実です。
対応: TypeScript に追跡させましょう。
危険信号 — 立ち止まって再検討する
- 網羅性チェックなしでユニオン型を switch している
- 何もせずに処理を終える default ケースがある
- すべての使用箇所を確認せずにユニオンへバリアントを追加している
- 「TODO: 新しいケースを処理する」というコメントがある
よくある正当化(すべて誤り)
| 言い訳 | 実際のところ |
|---|---|
| 「更新するのを忘れない」 | あなた、またはチームメイトが忘れます |
| 「default で未知のケースを処理できる」 | 新しいバリアントによるバグを隠します |
| 「switch は1つだけ」 | ユニオン型には、多くの場合、複数の switch があります |
クイックリファレンス
// The pattern
function assertUnreachable(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
// In switch statements
switch (union.type) {
case 'a': /* ... */ break;
case 'b': /* ... */ break;
default:
assertUnreachable(union); // Type error if cases missing
}
要点
never を使って、ケースの見落としをコンパイル時エラーにします。
タグ付きユニオンを処理するときは、default ケースに assertUnreachable(value) を追加します。これにより、ユニオンに新しいバリアントを追加すると、そのユニオンを処理するすべての箇所で型エラーが発生し、対応漏れによるエラーを実行時ではなくコンパイル時に捕捉できます。
参考資料
Dan Vanderkam 著「Effective TypeScript」の項目 59「網羅性チェックに Never 型を使用する」に基づいています。