TypeScript の型の中でも、初心者がつまずきやすいのが never 型です。「値を1つも持てない型」「決して起こらないことを表す型」と説明されますが、これだけではイメージしにくいでしょう。実は never は、必ず例外を投げる関数の戻り値や、絞り込みの結果として自然に現れます。そしてこの型を活用すると、ユニオン型のすべてのケースを処理し忘れていないかを型でチェックできる、とても実用的なテクニックにつながります。この記事では、never がどこで現れるのかと、その代表的な使い道である「網羅性チェック」を初心者向けに解説します。
目次
never 型とは「起こりえない」を表す型
never は「値が存在しえない」ことを表す型です。数値型 number には数値が、文字列型 string には文字列が入りますが、never 型にはどんな値も入りません。「絶対に値が返ってこない」「この状態には決してならない」といった状況を型で表現するために使われます。
let n: never; // never にはどんな値も代入できない // n = 1; // エラー // n = "text"; // エラー // n = null; // エラー
「何も代入できない型なんて、何に使うの?」と思うかもしれません。ポイントは、never は自分で書くよりも、TypeScript が自動的に導き出す場面が多い型だということです。まずはどんなときに現れるのかを見ていきましょう。
never が現れる場面
必ず例外を投げる関数
関数の中で必ず throw してしまい、正常に値を返すことがない関数の戻り値は never になります。「戻り値がない」ことを表す void と混同しやすいですが、void は「処理は最後まで進むが返す値がない」、never は「そもそも最後まで到達しない」という違いがあります。
// 必ず 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 ブロックの後では、input が null でない(= string)と正しく絞り込まれます。
絞り込みで可能性がなくなったとき
型の絞り込みを重ねて、「もう残っている型がない」状態になると、その値の型は never になります。次の例では、string でも number でもないと確定した else の中で、value は never になります。
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 で処理するとき、「すべてのケースを漏れなく書けているか」を型の力で保証できます。まず、チェックを入れない場合を見てみましょう。
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 型の変数に代入する一文を加えます。
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 に残り、status は never ではなくなるため、この行がコンパイルエラーになります。つまり「処理し忘れているケースがある」ことを、実行前に型エラーとして教えてくれるわけです。
型を追加したときに恩恵がある
網羅性チェックの本当の価値は、あとからユニオン型に選択肢が増えたときに発揮されます。たとえば Status に "idle" を追加したとします。
// "idle" を追加した type Status = "success" | "error" | "loading" | "idle"; // getMessage 側で "idle" の case を追加していないと、 // default の const exhaustiveCheck: never = status; がエラーになる // → 「対応を追加し忘れている」と気づける
網羅性チェックを入れておけば、"idle" に対応する case を追加し忘れた瞬間に型エラーが出ます。ユニオン型を扱う箇所が多いほど、この「追加漏れを型が教えてくれる」仕組みは大きな安心につながります。手作業では見落としがちな修正漏れを、コンパイラが確実に拾ってくれるのです。
まとめ
never は「値を1つも持てない・決して起こらない」ことを表す特殊な型です。必ず例外を投げる関数の戻り値や、絞り込みですべての可能性を潰したときに自然に現れます。そして最大の使いどころが、switch の default で「残った値を never に代入する」ことで実現する網羅性チェックです。これを入れておけば、ユニオン型のケースを書き忘れたときや、あとから選択肢が増えたときに、型エラーとして確実に気づけます。値としては使い道がなさそうな never ですが、「ありえない状態」を型で表現できるからこそ、コードの安全性を高める強力な武器になります。