1. ホーム
  2. TypeScript

【TypeScript】never 型の使い方|到達しないコードと網羅性チェックを解説

Share

TypeScript の型の中でも、初心者がつまずきやすいのが never 型です。「値を1つも持てない型」「決して起こらないことを表す型」と説明されますが、これだけではイメージしにくいでしょう。実は never は、必ず例外を投げる関数の戻り値や、絞り込みの結果として自然に現れます。そしてこの型を活用すると、ユニオン型のすべてのケースを処理し忘れていないかを型でチェックできる、とても実用的なテクニックにつながります。この記事では、never がどこで現れるのかと、その代表的な使い道である「網羅性チェック」を初心者向けに解説します。

never 型とは「起こりえない」を表す型

never は「値が存在しえない」ことを表す型です。数値型 number には数値が、文字列型 string には文字列が入りますが、never 型にはどんな値も入りません。「絶対に値が返ってこない」「この状態には決してならない」といった状況を型で表現するために使われます。

never-basic.ts
let n: never;

// never にはどんな値も代入できない
// n = 1;        // エラー
// n = "text";   // エラー
// n = null;     // エラー

「何も代入できない型なんて、何に使うの?」と思うかもしれません。ポイントは、never自分で書くよりも、TypeScript が自動的に導き出す場面が多い型だということです。まずはどんなときに現れるのかを見ていきましょう。

never が現れる場面

必ず例外を投げる関数

関数の中で必ず throw してしまい、正常に値を返すことがない関数の戻り値は never になります。「戻り値がない」ことを表す void と混同しやすいですが、void は「処理は最後まで進むが返す値がない」、never は「そもそも最後まで到達しない」という違いがあります。

throw-error.ts
// 必ず throw するので、この関数は値を返さない → 戻り値は never
function throwError(message: string): never {
  throw new Error(message);
}

function getValue(input: string | null): string {
  if (input === null) {
    // never を返す関数を呼ぶと、この先には進まないと TypeScript が理解する
    throwError("input が null です");
  }
  // ここでは input は string に絞り込まれている
  return input.toUpperCase();
}

throwError の戻り値を never にしておくと、それを呼んだ後のコードは「実行されない」と TypeScript が理解します。そのため if ブロックの後では、inputnull でない(= string)と正しく絞り込まれます。

絞り込みで可能性がなくなったとき

型の絞り込みを重ねて、「もう残っている型がない」状態になると、その値の型は never になります。次の例では、string でも number でもないと確定した else の中で、valuenever になります。

narrowing-never.ts
function handle(value: string | number) {
  if (typeof value === "string") {
    // value は string
  } else if (typeof value === "number") {
    // value は number
  } else {
    // string でも number でもない → value は never
    // (通常ここには到達しない)
  }
}

この「すべての可能性を潰すと never になる」という性質こそ、次に紹介する網羅性チェックの土台になります。

網羅性チェック(exhaustiveness check)に使う

never のもっとも実用的な使い道が、網羅性チェックです。ユニオン型の値を switch で処理するとき、「すべてのケースを漏れなく書けているか」を型の力で保証できます。まず、チェックを入れない場合を見てみましょう。

without-check.ts
type Status = "success" | "error" | "loading";

function getMessage(status: Status): string {
  switch (status) {
    case "success":
      return "成功しました";
    case "error":
      return "エラーが発生しました";
    // "loading" を書き忘れている!
  }
  // すべての case を返していないと、暗黙的に undefined が返る可能性がある
  return "";
}

この状態では "loading" の処理を書き忘れていても、エラーにならず気づけません。あとで Status に新しい値を追加したときも、対応を忘れたまま見過ごしてしまいます。そこで、default ブロックで「残った値」を never 型の変数に代入する一文を加えます。

with-check.ts
type Status = "success" | "error" | "loading";

function getMessage(status: Status): string {
  switch (status) {
    case "success":
      return "成功しました";
    case "error":
      return "エラーが発生しました";
    case "loading":
      return "読み込み中です";
    default:
      // ここに到達するなら status は never のはず。
      // もし case が足りないと、status は never でないためエラーになる
      const exhaustiveCheck: never = status;
      return exhaustiveCheck;
  }
}

すべての case を書いていれば、default に来る値は「もうありえない」ので never 型になり、const exhaustiveCheck: never = status; はエラーになりません。逆に case を1つでも書き忘れると、その値が default に残り、statusnever ではなくなるため、この行がコンパイルエラーになります。つまり「処理し忘れているケースがある」ことを、実行前に型エラーとして教えてくれるわけです。

型を追加したときに恩恵がある

網羅性チェックの本当の価値は、あとからユニオン型に選択肢が増えたときに発揮されます。たとえば Status"idle" を追加したとします。

add-case.ts
// "idle" を追加した
type Status = "success" | "error" | "loading" | "idle";

// getMessage 側で "idle" の case を追加していないと、
// default の const exhaustiveCheck: never = status; がエラーになる
// → 「対応を追加し忘れている」と気づける

網羅性チェックを入れておけば、"idle" に対応する case を追加し忘れた瞬間に型エラーが出ます。ユニオン型を扱う箇所が多いほど、この「追加漏れを型が教えてくれる」仕組みは大きな安心につながります。手作業では見落としがちな修正漏れを、コンパイラが確実に拾ってくれるのです。

まとめ

never は「値を1つも持てない・決して起こらない」ことを表す特殊な型です。必ず例外を投げる関数の戻り値や、絞り込みですべての可能性を潰したときに自然に現れます。そして最大の使いどころが、switchdefault で「残った値を never に代入する」ことで実現する網羅性チェックです。これを入れておけば、ユニオン型のケースを書き忘れたときや、あとから選択肢が増えたときに、型エラーとして確実に気づけます。値としては使い道がなさそうな never ですが、「ありえない状態」を型で表現できるからこそ、コードの安全性を高める強力な武器になります。

参考ページ