TypeScript | 基礎文法:関数の基礎 – never型の基礎

TypeScript
スポンサーリンク

never型は「絶対に起こらない」を表す型

まず一番大事なイメージからいきます。
never 型は、「ここには“値が存在しないはずだ”」「ここには“絶対に到達しないはずだ”」という場所を型で表すための特別な型です。

stringnumber は「こういう値が入るよ」という型ですが、
never は逆に「ここに値が入ることはありえない」という“ゼロ”の型です。

だからこそ、普通にコードを書いているときにはあまり出てきません。
でも、「ありえないはずのパターンを検出する」「絶対に返ってこない関数を表す」ときに、めちゃくちゃ役に立ちます。


基本1:絶対に戻ってこない関数の戻り値型としての never

例:必ず例外を投げる関数

function fail(message: string): never {
  throw new Error(message);
}
TypeScript

この関数は、throw した時点で処理が止まり、呼び出し元には「戻ってこない」関数です。
だから戻り値型は never になります。

ポイントは、void との違いです。

void は「戻り値を使わないけど、関数自体はちゃんと終わる」。
never は「関数が“正常には終わらない”ので、戻り値という概念自体がない」。

もう一つ例を出します。

function infiniteLoop(): never {
  while (true) {
    // ずっと回り続ける
  }
}
TypeScript

この関数も、呼び出したら最後、処理が戻ってきません。
こういう「絶対に終わらない」「必ず例外で止まる」関数の戻り値型が never です。


基本2:条件分岐の「漏れ」を検出するための never

ユニオン型と switch の組み合わせ

never が本領発揮するのは、「本当はありえないはずのケースが、実は漏れている」ことを教えてくれるときです。

例えば、こんなユニオン型があるとします。

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };
TypeScript

これを kind で分岐して処理したいとき、こう書きます。

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
  }
}
TypeScript

今はこれでいいのですが、あとから型をこう変えたとします。

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "triangle"; base: number; height: number };
TypeScript

でも、area 関数の中身を直し忘れたらどうなるでしょう?
triangle の処理が抜けたままになります。

ここで never を使った「抜け漏れ検出テクニック」が効いてきます。


応用:neverを使った「網羅性チェック」

最後に「ありえないはず」の場所を作る

さっきの area 関数を、こう書き換えます。

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      return assertNever(shape);
  }
}

function assertNever(x: never): never {
  throw new Error(`Unexpected object: ${JSON.stringify(x)}`);
}
TypeScript

ここでのポイントは assertNever の引数の型です。

function assertNever(x: never): never
TypeScript

xnever 型でなければならない」と宣言しています。
つまり、「ここに渡される値は“ありえない”はずだ」という意味です。

ところが、Shapetriangle を追加したのに switchcase "triangle" を書き忘れると、
default に渡ってくる shape の型は never ではなく { kind: "triangle"; ... } になります。

すると、ここでコンパイルエラーになります。

Type ‘{ kind: “triangle”; base: number; height: number; }’ is not assignable to type ‘never’.

これがめちゃくちゃ重要で、
「本当は“ありえないはず”の場所に、実は新しいパターンが流れ込んでいる」ことを、TypeScript が教えてくれるわけです。

このテクニックは「網羅性チェック(exhaustive check)」と呼ばれていて、
ユニオン型を使うときの定番パターンです。


neverは「何にでも代入できる」が「何からも代入されない」

ここは少しだけ理論寄りですが、感覚として知っておくと便利です。

never は「値が一つも存在しない型」なので、
「どんな型にも代入できる」が、「どんな型からも代入できない」という性質を持っています。

const n: never = 1;        // エラー: number は never に代入できない
const s: string = n;       // OK: never は string に代入できる
const u: { a: number } = n; // OK: never はどんな型にも代入できる
TypeScript

「どんな型にも代入できる」というのは、
「ここには絶対に値が来ない前提だから、何にでも“なれる”」というイメージです。

この性質があるからこそ、さっきの assertNever のような関数が書けます。

function assertNever(x: never): never {
  throw new Error("Unexpected value");
}
TypeScript

x に何かが渡ってきた時点で、「それはおかしい」と分かる。
「ここに値が来るのはバグだ」という場所を、型で表現できるのが never の強さです。


初心者がまず掴んでおきたい never の感覚

いきなり全部を使いこなそうとしなくて大丈夫です。
まずは、こんなふうに捉えておくといいです。

never は、「ここには値が来ないはず」「ここには到達しないはず」という“ありえない場所”を表す型。
必ず例外を投げる関数や、無限ループする関数の戻り値型として現れる。
ユニオン型と switch を組み合わせたときに、「分岐漏れ」を検出するためのトリガーとして使える。

そして一番大事なのは、
「never が出てきたら、“ここは本当にありえない場所のはずなんだけどな?”と立ち止まる」という姿勢です。

TypeScript が never を見せてくるとき、
それはたいてい「設計上の“ありえないはず”が、本当に守られているか?」を問いかけてきている瞬間です。
そこに気づけるようになると、never はただの難しそうな型ではなく、
「バグの芽を教えてくれるセンサー」に見えてきます。

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