【実務・中級編】関数型における「Never」型の使いどころ:例外スロー専用関数の定義 – TypeScript コア・型システムの基礎解析バイブル

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(data: unknown, message: string): T {
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` で置換してこい。それが、エンジニアとしての格を上げる第一歩だ。

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