【実務・中級編】Type Aliasによる「判別可能な共用体」の網羅性チェック(Exhaustiveness Checking) – TypeScript コア・型システムの基礎解析バイブル

TypeScriptの「網羅性チェック」を極める:`never`型でバグを設計段階で封殺せよ

フロントエンドの複雑性が増す現代のWeb開発において、最も避けるべきは「予期せぬ状態」への到達です。APIレスポンスのステータス管理、あるいは複雑なUIコンポーネントの分岐処理。これらを `switch` 文で書く際、あなたは「もし新しい状態が追加されたらどうなるか」をコンパイラに委ねられていますか?

「動けばいい」コードは、半年後の自分を殺す地雷になります。今回は、TypeScriptの型システムを最大限に活用し、「未定義のケース」をコンパイルエラーとして強制的に弾き出す、網羅性チェック(Exhaustiveness Checking)の極意を伝授します。

—

なぜ `default` ブランチだけでは不十分なのか

多くのエンジニアがやりがちなのが、これです。

type Status = ‘pending’ | ‘success’ | ‘error’;

function handleStatus(status: Status) {
switch (status) {
case ‘pending’: return ‘処理中…’;
case ‘success’: return ‘成功!’;
case ‘error’: return ‘失敗…’;
default:
// ここでエラーを投げるか、ログを出すのが一般的だが…
throw new Error(‘予期せぬ状態’);
}
}

このコードの脆弱性は明白です。もし将来的に `Status` 型に `’loading’` が追加されたとしても、この `switch` 文はコンパイルエラーを出しません。ランタイムで `default` に落ちるまで、開発者はバグに気づけないのです。

これが「型の安全性」を放棄した設計の末路です。

—

`never` 型による「コンパイル時強制」の極意

TypeScriptには、「決して到達しないはずのコード」を表す `never` 型が存在します。これを利用して、型システムの整合性を強制します。

以下のパターンをテンプレートとして記憶してください。

/

  • 網羅性チェック用ユーティリティ
  • 型システムが「ここに到達したならば、すべてのケースを処理し終えているはずだ」と判断する

/
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}

type Status = ‘pending’ | ‘success’ | ‘error’;

function handleStatus(status: Status) {
switch (status) {
case ‘pending’: return ‘処理中…’;
case ‘success’: return ‘成功!’;
case ‘error’: return ‘失敗…’;
default:
// ここで value はすでに never 型として推論される
return assertNever(status);
}
}

なぜこれで最強なのか

1. 型の伝播: `switch` 文の各ケースを通った後、残りの `status` は `never` 型として絞り込まれます。
2. 静的解析の活用: もし `Status` に新しい型が追加されたら、`default` ブランチ内の `status` はもはや `never` ではなくなります。結果、`assertNever(status)` の箇所で「型 ‘xxx’ を ‘never’ に割り当てられない」というコンパイルエラーが発生します。
3. 副作用なし: `assertNever` はランタイムでは決して呼ばれません(理想的なコードであれば)。もし呼ばれたとしても、それはプログラマの設計漏れを明確に示唆するデバッグのヒントになります。

—

実務での応用:APIレスポンスの型安全な分解

非同期通信の結果を扱う際、このパターンは最強の武器になります。

type RemoteData =
| { status: ‘idle’ }
| { status: ‘loading’ }
| { status: ‘success’; data: T }
| { status: ‘error’; error: Error };

function renderUI(state: RemoteData) {
switch (state.status) {
case ‘idle’: return null;
case ‘loading’: return ;
case ‘success’: return ;
case ‘error’: return ;
default:
// ここで state は never になる。
// もし将来 ‘retry’ ステータスが増えたら、即座にコンパイルエラーで教えてくれる。
return assertNever(state);
}
}

パフォーマンスと設計上の注意点

この手法において、パフォーマンス上の懸念は皆無です。`assertNever` は単なる関数の呼び出しであり、TypeScriptの型システムはこれらをコンパイル時にのみ解決します。

ただし、一点だけ注意してください。
「過度な網羅性チェックはコードを冗長にする」という点です。
全ての分岐でこれを行う必要はありません。ビジネスロジックの中核、特に「状態遷移」や「APIのレスポンスハンドリング」など、壊れると致命的な箇所に絞って適用するのが、熟練したアーキテクトの矜持というものです。

最後に:エンジニアとしての思考

型システムは単なる制約ではありません。「未来の自分やチームメンバーが、どうコードを拡張しても安全であるようにするための守護神」です。

今日、この `assertNever` を取り入れた瞬間から、あなたのコードベースにおける「未知のステートによるバグ」は過去のものになります。さあ、今すぐ `switch` 文を見直してみてください。そこに隠れた「未定義の未来」を、コンパイラに突きつけましょう。

タイトルとURLをコピーしました