メインコンテンツまでスキップ

第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-checkunion/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);
}

ポイントはここ👇😍

  • switchcaseが全部揃ってるなら、最後の statusnever になって assertNever(status) が通る
  • どれか漏れてると、statusnever にならなくて コンパイルエラーになる🔥

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 を増やすときに、漏れやすい箇所をリストアップして。switchRecord の両方で」📝
  • 「この 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寄り仕様にして、漏れゼロ設計を体に染み込ませるのも超おすすめだよ〜😆🧡