メインコンテンツまでスキップ

never型

never型は「値を持たない」を意味するTypeScriptの特別な型です。

neverの特性

何も代入できない

never型には何も代入できません。

ts
const foo: never = 1;
Type 'number' is not assignable to type 'never'.2322Type 'number' is not assignable to type 'never'.
ts
const foo: never = 1;
Type 'number' is not assignable to type 'never'.2322Type 'number' is not assignable to type 'never'.

たとえany型でも代入は不可能です。

ts
const any: any = 1;
const foo: never = any;
Type 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.
ts
const any: any = 1;
const foo: never = any;
Type 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.

唯一never型は代入できます。

ts
const foo: never = 1 as never;
ts
const foo: never = 1 as never;

何にでも代入できる

never型はどんな型にも代入できます。

ts
const nev = 1 as never;
const a: string = nev; // 代入OK
const b: string[] = nev; // 代入OK
ts
const nev = 1 as never;
const a: string = nev; // 代入OK
const b: string[] = nev; // 代入OK

値が無いとは

never型の「値が無い」とはどういうことでしょうか。たとえば、例外が必ず発生する関数の戻り値です。戻り値は絶対に取れません。そのため、戻り値の型はnever型になります。

ts
function throwError(): never {
throw new Error();
}
ts
function throwError(): never {
throw new Error();
}

終了しない関数も戻り値がnever型になります。

ts
function forever(): never {
while (true) {} // 無限ループ
}
ts
function forever(): never {
while (true) {} // 無限ループ
}

作り得ない値もnever型になります。たとえば、number型とstring型の両方に代入可能な値は作れません。そのため、number型とstring型のインターセクション型never型になります。

ts
type NumberString = number & string;
type NumberString = never
ts
type NumberString = number & string;
type NumberString = never

void型とnever型の違い

void型はundefinedが代入できますが、neverは値を持てません。

ts
const ok: void = undefined;
const ng: never = undefined;
Type 'undefined' is not assignable to type 'never'.2322Type 'undefined' is not assignable to type 'never'.
ts
const ok: void = undefined;
const ng: never = undefined;
Type 'undefined' is not assignable to type 'never'.2322Type 'undefined' is not assignable to type 'never'.

意味的に戻り値でのvoidneverは、戻り値が無い点は同じです。関数が終了するかが異なります。voidは関数が最後まで実行されるという意味です。neverは関数の処理が中断、もしくは、永遠に続くことを意味します。

戻り値終了するか
void無いreturnされるか、最後まで実行される
never無い中断されるか、永遠に実行される

そのため、戻り値がneverの関数が最後まで到達できてしまう実装の場合、TypeScriptはコンパイルエラーを出します。

ts
function func(): never {}
A function returning 'never' cannot have a reachable end point.2534A function returning 'never' cannot have a reachable end point.
ts
function func(): never {}
A function returning 'never' cannot have a reachable end point.2534A function returning 'never' cannot have a reachable end point.

neverを使った網羅性チェック

neverの何も代入できないという特性は、網羅性チェック(exhaustiveness check)に応用できます。網羅性チェックとは、ユニオン型を分岐処理するとき、ロジックがすべてのパターンを網羅しているかをコンパイラにチェックさせることを言います。

たとえば、3パターンのユニオン型があるとします。

ts
type Extension = "js" | "ts" | "json";
ts
type Extension = "js" | "ts" | "json";

このうち2パターンにだけ対応した分岐処理が次です。これには網羅性がありませんが、TypeScriptは警告を出しません。

網羅性がない分岐
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
// "json"に対する分岐がない
}
}
網羅性がない分岐
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
// "json"に対する分岐がない
}
}

網羅性チェックの基本

網羅性チェックを行うには、default分岐で網羅性をチェックしたい値をnever型に代入します。すると、TypeScriptが代入エラーの警告を出すようになります。

網羅性チェックがついた分岐
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
const exhaustivenessCheck: never = ext;
Type 'string' is not assignable to type 'never'.2322Type 'string' is not assignable to type 'never'.
break;
}
}
網羅性チェックがついた分岐
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
const exhaustivenessCheck: never = ext;
Type 'string' is not assignable to type 'never'.2322Type 'string' is not assignable to type 'never'.
break;
}
}

例外による網羅性チェック

一歩進んで網羅性チェック用の例外クラスを定義するのがお勧めです。このクラスは、コンストラクタ引数にnever型を取る設計にします。

網羅性チェック関数
ts
class ExhaustiveError extends Error {
constructor(value: never, message = `Unsupported type: ${value}`) {
super(message);
}
}
網羅性チェック関数
ts
class ExhaustiveError extends Error {
constructor(value: never, message = `Unsupported type: ${value}`) {
super(message);
}
}

この例外をdefault分岐で投げるようにします。コンストラクタに網羅性をチェックしたい引数を渡します。こうしておくと、網羅性が満たされていない場合、TypeScriptが代入エラーを警告します。

ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
throw new ExhaustiveError(ext);
Argument of type 'string' is not assignable to parameter of type 'never'.2345Argument of type 'string' is not assignable to parameter of type 'never'.
}
}
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
throw new ExhaustiveError(ext);
Argument of type 'string' is not assignable to parameter of type 'never'.2345Argument of type 'string' is not assignable to parameter of type 'never'.
}
}

例外にしておく利点は2つあります。

  1. noUnusedLocalsに対応可能
  2. 実行時を意識したコードになる

noUnusedLocalsに対応可能

コンパイラオプションnoUnusedLocalsは使われていない変数について警告を出すかを設定します。これがtrueのとき、変数に代入するだけの網羅性チェックはコンパイルエラーになります。

全網羅するも未使用変数で警告される
ts
function func(value: "yes" | "no"): void {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
const exhaustivenessCheck: never = value;
'exhaustivenessCheck' is declared but its value is never read.6133'exhaustivenessCheck' is declared but its value is never read.
break;
}
}
全網羅するも未使用変数で警告される
ts
function func(value: "yes" | "no"): void {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
const exhaustivenessCheck: never = value;
'exhaustivenessCheck' is declared but its value is never read.6133'exhaustivenessCheck' is declared but its value is never read.
break;
}
}

網羅性チェックを例外にしておくと、未使用変数についてのコンパイルエラーが発生しなくなります。

実行時を意識したコードになる

例外のほうが、コンパイル後のJavaScriptを意識した実装になります。変数代入による網羅性チェックのコードをコンパイルすると、次のJavaScriptが生成されます。

コンパイル後のJavaScript(変数代入による網羅性チェック)
ts
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
const exhaustivenessCheck = value;
break;
}
}
 
コンパイル後のJavaScript(変数代入による網羅性チェック)
ts
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
const exhaustivenessCheck = value;
break;
}
}
 

コンパイルもとのTypeScriptを知らない者がこのコードを見ると、exhaustivenessCheckへの代入は意図が不明です。また、網羅性のチェックは実行時に行われません。

例外による網羅性チェックは、コンパイル後コードだけ見ても意図が明瞭です。また、実行時にもチェックが行われます。このほうがよい実装になります。

コンパイル後のJavaScript(例外による網羅性チェック)
ts
class ExhaustiveError extends Error {
constructor(value, message = `Unsupported type: ${value}`) {
super(message);
}
}
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
throw new ExhaustiveError(value);
}
}
 
コンパイル後のJavaScript(例外による網羅性チェック)
ts
class ExhaustiveError extends Error {
constructor(value, message = `Unsupported type: ${value}`) {
super(message);
}
}
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
throw new ExhaustiveError(value);
}
}
 
学びをシェアする

TypeScriptのneverは「値を持たない」型。

1️⃣特性1: neverへは何も代入できない
2️⃣特性2: neverは何にでも代入できる
💥常に例外を起こす関数の戻り値に使える
👐voidとは異なる
✅網羅性チェックに応用できる

『サバイバルTypeScript』より

この内容をツイートする