TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – neverを返す関数設計

TypeScript
スポンサーリンク

ゴール:「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());
}
TypeScript

fail の戻り値が never であることを TypeScript が理解しているので、
if (value === null) の中で fail を呼んだあとは、
「その分岐から先には進まない」と推論できます。

その結果、if の外では valuestring に絞り込まれます。

「ここに来たらバグ」の場所で使う

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); // ここに来たらバグ
  }
}
TypeScript

assertNever の引数は 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 ではなく
voidbooleanT などを返すべきです。

「この関数は“止める係”か?」
という問いを一度挟むと、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 は「よく分からない謎の型」から、
“ありえない・戻らない”をコードに刻み込むための、強力な設計ツール
に変わっていきます。

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