TypeScriptの深淵:`never`型で「実行不可能なコード」を型レベルで封殺する
コードレビューをしていると、しばしば「このパスには到達しないはずだ」という強い確信に基づいた、しかし型システムがそれを保証できていないコードに出会う。
`// TODO: ここには来ないはず` とコメントを書くその瞬間に、君のコードには脆弱性が宿っている。TypeScriptは、君が「到達不可能である」と主張するコードを、型システムレベルで完全に消し去る術を持っている。それが`never`型だ。
今回は、単に「エラーを投げる関数」を`never`で定義するという表面的な話を超え、なぜこれが堅牢なシステム設計において不可欠なピースなのかを解説する。
—
1. なぜ `void` ではなく `never` なのか?
多くの開発者は、例外を投げる関数に`void`を割り当てる。しかし、これは型システムに対する「嘘」だ。
// 悪い例:voidを返すと宣言している
function panic(message: string): void {
throw new Error(message);
}
function process(input: string | null) {
if (input === null) panic(“Input missing”);
// ここでTypeScriptは「panicが戻り値を返さない可能性がある」ことを検知できず、
// 後続のコードでinputがnullである可能性を考慮し続ける羽目になる。
console.log(input.length); // TSエラー:’input’ is possibly ‘null’.
}
`void`は「何も返さない(値としては`undefined`を持つ)」ことを意味する。一方で`never`は「戻り値が存在する概念そのものが存在しない」ことを意味する。コンパイラに対し、「この関数の呼び出し以降、制御フローは停止し、以降の行は実行されない」という事実を物理的に刻み込むのだ。
—
2. 実践:到達不能パスを「型」で制御する
プロダクションコードにおいて、`never`を最も美しく使う場面は「網羅性チェック(Exhaustiveness Checking)」だ。`switch`文や`if-else`で、全ての条件を処理しきったはずなのに、未来の誰かが新しいケースを追加した時に型エラーで警告を出す設計ができる。
type Action = { type: ‘LOGIN’ } | { type: ‘LOGOUT’ } | { type: ‘REFRESH’ };
function unreachable(x: never): never {
throw new Error(`到達不可能なはずのコードが実行されました: ${x}`);
}
function handleAction(action: Action) {
switch (action.type) {
case ‘LOGIN’:
return ‘Logged in’;
case ‘LOGOUT’:
return ‘Logged out’;
// case ‘REFRESH’: を書き忘れると…
default:
// ここで action は ‘REFRESH’ 型に絞り込まれる
// もし ‘REFRESH’ を処理し忘れていれば、never型ではないものが渡され、コンパイルエラーになる
return unreachable(action);
}
}
この設計の強みは、「仕様変更が型エラーとして通知される」ことにある。`Action`型に新しい値を追加した瞬間、`handleAction`の`unreachable`箇所で即座にコンパイルエラーが発生する。ドキュメントを読み直す必要はない。型システムが君の設計の隙を教えてくれるのだ。
—
3. 非同期API連携における「安全なフォールバック」
フロントエンドでありがちな、APIレスポンスの型ガードとエラーハンドリングの融合にも`never`が役立つ。
/
- APIレスポンスを検証し、失敗した場合は即座に例外を投げる
- 成功した場合は、レスポンスの型を保証して呼び出し元へ戻す
/
function assertResponse
if (data === null || typeof data !== ‘object’) {
throw new Error(message);
}
return data as T;
}
// 使用例
async function fetchUser() {
const res = await fetch(‘/api/user’);
const data = await res.json();
// 型チェックと同時にエラーハンドリングを完了させる
const user = assertResponse<{ id: string }>(data, “Invalid user format”);
// ここから先は user が必ず { id: string } 型であることを保証できる
return user.id;
}
もし`assertResponse`が例外を投げず、そのまま処理を続行しようとすれば、TypeScriptの制御フロー解析が「到達可能性」を追跡し、`never`の欠如を指摘するだろう。
—
4. チーフアーキテクトからの助言:パフォーマンスと設計の美学
「`never`を使うとパフォーマンスは変わるのか?」という問いに対しては、「ランタイムのオーバーヘッドはないが、開発時の生産性は劇的に向上する」と答える。
コンパイラは`never`型を認識した瞬間、そのブランチの型解析を打ち切る。これにより、複雑な条件分岐が重なる大規模プロジェクトにおいても、無駄な型推論のコストを削減できる。
まとめ:プロダクションコードへの適用ルール
1. 例外を投げる関数には必ず `: never` を付与せよ。これはコンパイラへの強力なヒントになる。
2. `default` ブランチには常に `unreachable` 関数を置け。将来の自分やチームメンバーがバグを仕込む余地を、コンパイラという番人に監視させるのだ。
3. 「直感」を「コードの事実」に変換せよ。コード内のコメントアウトで説明するのではなく、型システムにルールを記述させる。
TypeScriptは単なるJavaScriptの補完ツールではない。君のロジックが論理的に正しいことを証明するための、数学的なエンジンだ。`never`を使いこなせば、君のコードは「壊れない」という確信に裏打ちされた、最高に美しいプロダクションコードへと昇華する。
さあ、今すぐコードベース内の `// TODO` を検索し、それを `never` で置換してこい。それが、エンジニアとしての格を上げる第一歩だ。