ゴール:「never を返す関数」が“設計上どういう意味を持つか”を理解する
never は、初心者から見ると一番「意味不明な型」に見えます。
でも、関数設計の文脈では、
「ここには絶対に到達しない」「この関数は絶対に戻ってこない」
という、とても強いメッセージを表現できる型です。
ここでは、
neverとは何か- どんな関数が「never を返す」と言えるのか
- 実際の設計で
neverをどう使うと気持ちよくなるか
を、例を交えながら丁寧にかみ砕いていきます。
そもそも never とは何か
「絶対に値が存在しない」型
never は、TypeScript の中で
「絶対に値が存在しない型」
として定義されています。
const x: never = 1; // エラー
TypeScriptどんな値も never には代入できません。
では、関数の戻り値型としての never は何を意味するかというと、
「この関数は、正常には“戻ってこない”」
という宣言です。
function f(): never {
// ここから先、呼び出し元に制御が戻ることはない
}
TypeScript「戻らない」とはどういうことか
「戻らない」と言われてもピンとこないかもしれませんが、
具体的には次のようなパターンがあります。
- 例外を投げて必ず throw で終わる
- 無限ループに入り、関数が終わらない
- プロセスを終了させる(ブラウザならほぼないが、Node.js ならありうる)
こういう関数は、
「呼び出したら最後、呼び出し元に制御が返ってこない」
ので、戻り値の型は never と表現できます。
典型例1:必ず例外を投げる関数
エラーを投げるだけの関数
一番分かりやすいのがこれです。
function fail(message: string): never {
throw new Error(message);
}
TypeScriptこの関数は、絶対に return しません。
必ず throw で終わります。
呼び出し側から見ると、
function doSomething(value: string | null) {
if (value === null) {
fail("value is null");
}
// ここに来るとき、value は string であることが保証される
console.log(value.toUpperCase());
}
TypeScriptfail の戻り値が never であることを TypeScript が理解しているので、if (value === null) の中で fail を呼んだあとは、
「その分岐から先には進まない」と推論できます。
その結果、if の外では value が string に絞り込まれます。
「ここに来たらバグ」の場所で使う
never を返す関数は、
「ここに来るのはおかしい」という場所を明示する
ためにも使えます。
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function assertNever(x: never): never {
throw new Error("Unexpected value: " + JSON.stringify(x));
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius * shape.radius;
case "square":
return shape.size * shape.size;
default:
return assertNever(shape); // ここに来たらバグ
}
}
TypeScriptassertNever の引数は never 型です。
つまり、「ここに渡される値は本来存在しないはず」という意味です。
もし将来 Shape に新しいバリアントを追加してswitch に書き忘れた場合、assertNever(shape) のところでコンパイルエラーになります。
「列挙漏れをコンパイル時に検出するための“保険”」として、never を返す関数が使える
というのが、かなり重要なパターンです。
典型例2:無限ループする関数
終わらないループ
もう1つのパターンは「無限ループ」です。
function loopForever(): never {
while (true) {
// 何かし続ける
}
}
TypeScriptこの関数も、呼び出し元に戻ることはありません。
なので、戻り値型は never で表現できます。
実務でこの形をそのまま使うことは少ないですが、
「イベントループを回し続ける」「サーバーを立ち上げて待ち続ける」
といった処理では、概念的には never に近い動きをします。
never を返す関数が「設計上」持っている意味
1. 「ここから先には進まない」ということを型で表現する
never を返す関数を使うと、
「この行のあとには、制御が戻ってこない」
という事実を、型システムに教えられます。
さっきの fail の例をもう一度見ます。
function fail(message: string): never {
throw new Error(message);
}
function doSomething(value: string | null) {
if (value === null) {
fail("value is null");
}
// ここでは value は string として扱える
console.log(value.toUpperCase());
}
TypeScriptもし fail の戻り値型を void にしてしまうと、
TypeScript は「ここから先に進まない」とは判断できません。
function failVoid(message: string): void {
throw new Error(message);
}
function doSomething2(value: string | null) {
if (value === null) {
failVoid("value is null");
}
// ここで value は string | null のまま
console.log(value.toUpperCase()); // エラー
}
TypeScript「この関数は絶対に戻らない」と型で宣言することで、
その後のコードの型推論が賢くなる
というのが、never を返す関数の大きな価値です。
2. 「ここに来たらバグ」を明示する
assertNever パターンは、
「この分岐に来るのは設計上ありえない」
ということを、コードと型の両方で表現します。
function assertNever(x: never): never {
throw new Error("Unexpected value: " + JSON.stringify(x));
}
TypeScriptこの関数を default 分岐で呼ぶことで、
- 実行時には「ありえない値が来た」ときに例外を投げる
- コンパイル時には「型の列挙漏れ」を検出できる
という二重の安全装置になります。
「ここに来たらバグ」という場所をassertNever でマークしておくのは、
“未来の自分へのメッセージ”としても非常に強力 です。
never を返す関数を自分で設計するときの考え方
「この関数は、本当に戻ってこないべきか?」
never を戻り値に使うのは、かなり強い宣言です。
なので、まず自分にこう聞いてください。
「この関数は、正常系として“戻ってくる”可能性があるか?」
もし少しでも「ある」と思うなら、never は使うべきではありません。
例えば、次のような関数は never にすべきではありません。
function maybeFail(): never {
if (Math.random() > 0.5) {
throw new Error("fail");
}
console.log("success");
}
TypeScriptこれは「成功することもある」ので、
戻り値型を never にするのは嘘です。
never を返す関数は、
- 必ず throw で終わる
- 必ず無限ループに入る
など、「絶対に戻らない」と言い切れるものだけに使うべきです。
「この関数の“役割”は、制御を止めることか?」
never を返す関数は、
「制御を止める」「ここで処理を打ち切る」
という役割を持ちます。
典型的には、
- エラーを投げて処理を中断する
- ありえない状態に対して例外を投げる
- プロセスを終了させる
といった用途です。
逆に言うと、
- ログを出すだけ
- 値を検証して true / false を返す
- 状態を更新するだけ
といった関数は、never ではなくvoid や boolean、T などを返すべきです。
「この関数は“止める係”か?」
という問いを一度挟むと、never を使うべきかどうかが見えてきます。
実務でよく使う never 関数のテンプレート
エラー専用関数
function panic(message: string): never {
throw new Error(message);
}
TypeScript使い方:
function getUserName(user: { name?: string }) {
if (!user.name) {
return panic("name is missing");
}
return user.name;
}
TypeScript列挙漏れ検出用の assertNever
function assertNever(x: never, message = "Unexpected value"): never {
throw new Error(`${message}: ${JSON.stringify(x)}`);
}
TypeScript使い方:
type Status = "idle" | "loading" | "success";
function handleStatus(status: Status) {
switch (status) {
case "idle":
// ...
break;
case "loading":
// ...
break;
case "success":
// ...
break;
default:
assertNever(status);
}
}
TypeScriptまとめ:never を返す関数設計を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
never を返す関数は、
「この関数は、呼び出し元に制御が戻ることはない」
「ここに来たら処理は終わり(もしくはバグ)」
という、非常に強い宣言を持つ。
典型的には、
- 必ず例外を投げる関数(
fail,panicなど) - ありえない状態に対して例外を投げる
assertNever - 無限ループする関数
に使う。
設計するときは、
「この関数は本当に戻ってこないべきか?」
「この関数の役割は“処理を止めること”か?」
を一度自分に問いかけてから、
それでも「はい」と言えるときだけ never を選ぶ。
そうやって意図的に使うと、never は「よく分からない謎の型」から、
“ありえない・戻らない”をコードに刻み込むための、強力な設計ツール
に変わっていきます。
