こんにちは。TypeScriptという言語の深淵へようこそ。
日々の開発で「もし新しい状態が増えたら、修正漏れが起きそうだな……」と不安になったことはありませんか?その不安、実はTypeScriptの「型システム」を正しく使えば、コンパイラにすべて肩代わりさせることができるんです。
今日は、TypeScriptにおける最も美しく、かつ強力な防御策の一つ、「判別可能な共用体(Discriminated Unions)による網羅性チェック」について、その本質を解き明かしていきましょう。
—
なぜ「網羅性チェック」が必要なのか?
例えば、ユーザーの権限を管理するシステムを作るとしましょう。
type UserRole = “admin” | “editor” | “viewer”;
function getAccessLevel(role: UserRole): string {
switch (role) {
case “admin”: return “フルアクセス”;
case “editor”: return “編集可能”;
case “viewer”: return “閲覧のみ”;
// もし将来 “guest” が追加されたら?
}
}
このコード、一見問題なさそうですよね。しかし、もし将来要件が変わって `UserRole` に `”guest”` が追加されたらどうなるでしょうか? `getAccessLevel` 関数は何も返さず、実行時に `undefined` が返ってきて予期せぬバグを引き起こすかもしれません。
これを「コンパイル時に検知して、修正を強制する」のが、今回のテーマです。
—
魔法の型 `never` を使った防御壁
TypeScriptには、型理論における「空集合」、つまり「決してあり得ない値」を表す `never` 型が存在します。これを利用して、網羅性を保証します。
実践:網羅性チェックの書き方
まずは、網羅性をチェックするための「トドメのコード」を見てください。
function getAccessLevel(role: UserRole): string {
switch (role) {
case “admin”: return “フルアクセス”;
case “editor”: return “編集可能”;
case “viewer”: return “閲覧のみ”;
default:
// ここに到達した時点で、コンパイラに「ここには何も来るはずがない」と教える
const _exhaustiveCheck: never = role;
return _exhaustiveCheck;
}
}
このコードで何が起きているのか?
1. `switch` の絞り込み: `case` 文を通るたびに、TypeScriptは `role` 型を絞り込みます。全てのケースを通れば、`default` ブロックに残っている `role` は `never` 型になります。
2. `never` への代入: もし `role` が `never` 以外(例えば追加された `”guest”` など)の型を持っていた場合、TypeScriptは「`string` や `undefined` を `never` 型には代入できません!」とコンパイルエラーを吐いてくれます。
これが、開発者が寝ている間もコンパイラがコードを監視し続けてくれる理由です。
—
判別可能な共用体(Discriminated Unions)の真価
先ほどの例は単純な文字列リテラルでしたが、実際の現場ではオブジェクトの構造を判別させることがほとんどです。
type Admin = { type: “admin”; level: number };
type Editor = { type: “editor”; permissions: string[] };
type Viewer = { type: “viewer” };
type User = Admin | Editor | Viewer;
function processUser(user: User) {
switch (user.type) {
case “admin”:
console.log(user.level); // ここでは user は Admin として扱われる
break;
case “editor”:
console.log(user.permissions); // ここでは user は Editor として扱われる
break;
case “viewer”:
// 処理
break;
default:
const _exhaustiveCheck: never = user; // 網羅性が崩れるとここでエラー!
}
}
ここで重要なのは、「`type` という共通のキー(判別子)を持っていること」です。TypeScriptは `user.type` を見るだけで、「ああ、これは `Admin` なんだな」と型を自動的に推論(ナローイング)してくれます。
—
陥りやすい罠とポイント
初学者がよくやってしまうミスを二つ紹介します。
1. `default` ブロックを「とりあえず」で書かない
網羅性チェックを行う場合、`default` ブロックは「バグを見つけるための検問所」です。「とりあえずここにエラーハンドリングを書いておこう」とすると、網羅性チェックが機能しなくなります。
2. コンパイル時と実行時の区別
このチェックはあくまで「コンパイル時」の静的解析です。実行時に想定外の値が渡ってくる可能性がある場合は、`default` ブロック内に実行時エラーを投げる処理を入れるのが堅牢な設計です。
default:
const _exhaustiveCheck: never = user;
throw new Error(`未対応のユーザー型です: ${JSON.stringify(_exhaustiveCheck)}`);
—
まとめ:TypeScriptを「最強の相棒」にする
この網羅性チェックをマスターすると、コードの修正が怖くなくなります。なぜなら、「漏れがあれば、コンパイラが真っ赤になって怒ってくれるから」です。
1. `type` 判別子を持つ共用体を作る
2. `switch` 文で全ケースを書く
3. `default` ブロックで `never` に代入して漏れを防ぐ
これだけで、あなたの書くコードの安全性は劇的に向上します。ここをクリアできれば、TypeScriptの型システムの「重み」と「慈悲深さ」の両方を理解できたと言えるでしょう。
さあ、次はどんな複雑な状態遷移を、この網羅性チェックで飼い慣らしてみますか?応援していますよ!