第38章:網羅性(exhaustive)で抜け漏れ防止🕳️🚫

状態(status)や種類(type)が増えてくると、switchの分岐追加を忘れてバグること、めっちゃあります🥲💦 そこでこの章は、「追加漏れ」を“コンパイルで気づける形”にして、事故を減らすのがゴールだよ〜!🧠🔒
🎯 目的
- union型(例:
"draft" | "paid" | "cancelled")や判別可能Unionで、分岐漏れをコンパイルエラーで検知できるようになる🧷✨ - 「テストで守る」と「型で守る」をセットで使い分けられるようになる🧪🛡️
- ESLintのルールも使って、チームでも漏れにくい運用にできる👭📏
📚 学ぶこと(この章のキモ💡)
1) never を使った “到達しないはず” チェック🧨
never は「ここに来るはずがない」型。
全部のcaseを書けてたら、最後に残る型は never になる → これを利用して漏れを検出するよ✨
2) switch の “網羅性” を「型で強制」するパターン✅
assertNever(x: never)を作るswitchのあとにassertNever(status)を置く → caseを追加し忘れた瞬間にコンパイルエラー🔥
3) Record<Union, ...> で “対応表” を網羅させる📒
UIラベルやメッセージは switch より 対応表の方が読みやすいことも多いよ〜😊
4) ESLintの switch-exhaustiveness-check で “人間のミス” を減らす🧹
@typescript-eslint/switch-exhaustiveness-check は union/enumのswitchでcase漏れを指摘してくれるよ📣
(ただし “defaultがあると指摘しない” 方向なので、運用が大事!) (typescript-eslint.io)
🧪 手を動かす:ミニ題材「注文ステータス」🍔🧾
題材の仕様(テストで固定するよ)🧪✨
注文ステータス OrderStatus があって、UI表示ラベルを返す statusLabel を作る!
"draft"→下書き"submitted"→送信済み"paid"→支払い済み
1) テストを書く(Red)🔴🧪
src/status.test.ts
import { describe, it, expect } from "vitest";
import { statusLabel, type OrderStatus } from "./status";
describe("statusLabel", () => {
it("draft は 下書き", () => {
const s: OrderStatus = "draft";
expect(statusLabel(s)).toBe("下書き");
});
it("submitted は 送信済み", () => {
const s: OrderStatus = "submitted";
expect(statusLabel(s)).toBe("送信済み");
});
it("paid は 支払い済み", () => {
const s: OrderStatus = "paid";
expect(statusLabel(s)).toBe("支払い済み");
});
});
2) 最小実装(Green)🟢✨
src/status.ts
export type OrderStatus = "draft" | "submitted" | "paid";
export function assertNever(x: never, message = "Unreachable"): never {
throw new Error(`${message}: ${String(x)}`);
}
export function statusLabel(status: OrderStatus): string {
switch (status) {
case "draft":
return "下書き";
case "submitted":
return "送信済み";
case "paid":
return "支払い済み";
}
// ここが “網羅性のカギ” 🔑✨
return assertNever(status);
}
ポイントはここ👇😍
switchの caseが全部揃ってるなら、最後のstatusはneverになってassertNever(status)が通る- どれか漏れてると、
statusがneverにならなくて コンパイルエラーになる🔥
3) 「状態を追加」して、漏れをコンパイルで検知してみる🧯🚨
ここでわざと仕様追加!✨
OrderStatus に "cancelled" を足すよ!
export type OrderStatus = "draft" | "submitted" | "paid" | "cancelled";
すると… statusLabel の最後の assertNever(status) が コンパイルで怒られるはず!😳⚡
「cancelled の case ないよ〜」ってことが、テスト実行前にバレるのが最高👏✨
じゃあ追加👇
case "cancelled":
return "キャンセル";
✨ 別解:Record 対応表で “必ず全部埋める” 📒🧷
UIラベルみたいな「対応表」は switch よりコレが読みやすいこと多いよ〜😊
export type OrderStatus = "draft" | "submitted" | "paid";
const LABELS = {
draft: "下書き",
submitted: "送信済み",
paid: "支払い済み",
} satisfies Record<OrderStatus, string>;
export function statusLabel(status: OrderStatus): string {
return LABELS[status];
}
これの良いところ💖
OrderStatusに追加したら、LABELSにキーがないと コンパイルエラーになる👏- 「漏れ」が起きにくいし、読みやすい〜📖✨
🧹 ESLintでさらに漏れを減らす(おすすめ)📣
@typescript-eslint/switch-exhaustiveness-check を有効にすると、union/enumのswitchでcase漏れを検出してくれるよ✨ (typescript-eslint.io)
ただし注意点⚠️
- このルールは「defaultが無いswitch」を前提に検出しがち(defaultがあると見逃しやすい)ので、 この章の“assertNeverをswitchの後ろに置く”形式と相性がいいよ😊
⭐ 発展:ts-pattern の .exhaustive() で “網羅性” をもっと書きやすく🎨
条件分岐が複雑になってくると、ts-pattern のパターンマッチが便利な場面もあるよ〜!
.exhaustive() をつけると 網羅できてないとエラーにできる✨ (GitHub)
(「switchがごちゃごちゃして読みにくい…🥲」って時の選択肢!)
🤖 AIの使い方(この章向けプロンプト例)💬🤖✨
コピペで使えるよ〜🫶
- 「
OrderStatusを増やすときに、漏れやすい箇所をリストアップして。switchとRecordの両方で」📝 - 「この
switchを “網羅性チェック付き” に直して(assertNever方式)」🧷 - 「UIラベル、色、ボタン表示可否を status ごとに返したい。
Recordと型で漏れなく書く形にして」🎨🔘 - 「このunionに1つ追加した想定で、壊れる箇所を予測して」🔮✨
✅ チェック(できたら合格🎉)
assertNever方式で、case漏れがコンパイルで検知できる✅Record<Union, ...>方式で、対応表のキー漏れがコンパイルで検知できる✅- 「UIラベルは対応表」「分岐ロジックはswitch」みたいに、読みやすさで選べる✅
- ESLintの
switch-exhaustiveness-checkの役割が説明できる✅ (typescript-eslint.io)
🧁 おまけ:2026年1月時点の“最新”メモ📌
- TypeScript の最新は **5.9系(npmのlatestは 5.9.3)**だよ🧪✨ (NPM)
- さらに先の話として、TypeScriptのネイティブ実装プレビュー(高速化の流れ)も進んでるみたい👀⚡ (Microsoft Developer)
次は、ここで作った OrderStatus をもう少し育てて、
「statusごとに 次に押せるボタン を変える」みたいなUI寄り仕様にして、漏れゼロ設計を体に染み込ませるのも超おすすめだよ〜😆🧡