never は値を1つも持たない型です。戻り値の型が never の関数は、普通に戻ることがありません。例外を投げるか、永遠に動き続けます。never 型の変数は実行されえないコードを示し、それによって網羅性チェックが可能になります。
never はあらゆる型に代入できるので、number を返す関数の中でも return fail(...) はコンパイルできます。never 型の値が実際に届くことはないからです。
戻らない関数
最後までたどり着かない関数は2種類あります。常に例外を投げる関数と、無限ループを持つ関数です。コンパイラーはその主張をチェックします。never と型注釈を付けた関数の終わりに到達できると、エラー TS2534 A function returning 'never' cannot have a reachable end point になります。
function fail(message: string): never {
throw new Error(message);
}
function runForever(): never {
while (true) {
// poll, serve requests...
}
}
推論は構文によって違います。例外を投げるだけの関数宣言は void を返すと推論され、例外を投げるだけのアロー関数や関数式は never と推論されます:
function f1() { throw new Error("x"); } // () => void
const f2 = () => { throw new Error("x"); }; // () => never
絞り込みで呼び出しを行き止まりとして扱うのは、呼び出した名前が never を返す明示的な型を持つ場合だけです。上の fail のように : never と型注釈を付けた関数宣言か、型注釈を付けた変数 const fail: (m: string) => never = (m) => { throw new Error(m); } です。推論された never は数に入らず、アロー関数にだけ型注釈があって変数にはない const fail = (m: string): never => ... も同じく数に入りません。
never と void
void | never | |
|---|---|---|
| 関数が終わるか | はい | いいえ(例外を投げるか無限ループ) |
| 実行時の値 | undefined | なし: 呼び出しが値を作ることはない |
| 呼び出しのあとのコード | 到達できる | 到達できない |
| ほかの型に代入できるか | void、unknown、any にだけ | すべての型に |
| よくある用途 | コールバック、イベントハンドラー、副作用のある関数 | fail()、assertNever()、無限ループ |
実用上の違いは絞り込みに表れます。if (!user) fail("no user") のあと、次の行で user が定義済みだとコンパイラーがわかるのは、fail が never を返す場合だけです。戻り値が void なら、実行が続く可能性があると考えます。
never による網羅性チェック
ユニオン型に対する switch の各 case は値を絞り込みます。すべてのメンバーを処理すると、default に残るのは never です。それを never 型の変数に代入すると、「すべてのケースを処理した」ことをコンパイラーが検証してくれます:
ここで、ケースを追加せずにユニオン型に3つ目のメンバーを追加してみます:
index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.
エラーが、忘れたメンバーを名指ししてくれます。case "triangle": return (shape.base * shape.height) / 2; を追加すれば再びコンパイルできます。assertNever ヘルパーは同じチェックを再利用できる形にしたもので、その throw は実行時にも意味があります。JSONや古いクライアントからのデータには、型の上ではありえない kind が含まれているかもしれないからです。このパターンは 判別可能なユニオン型 の土台です。
never まで絞り込む
switch だけでなく、どんな絞り込みでも同じことが起きます。すべての可能性が除外されると、変数は never 型になります:
あとで引数を string | number | boolean | bigint に広げると、const nothing: never = x の行がエラーになり、更新が必要な関数を指し示してくれます。
never はユニオン型の中で消える
never は値の空集合なので、ユニオン型に加えても何も変わりません。string | never はただの string です。条件型がユニオン型を絞り込むのはこの仕組みです。never を返すブランチは、そのメンバーを取り除きます:
組み込みの Exclude<T, U> と Extract<T, U> はまさにこの方法で動いています。交差型では逆で、string & never は never です。
ありえない型は never になる
どんな値も満たせない交差型は never に縮約されます:
type A = string & number; // never
type B = { kind: "a" } & { kind: "b" }; // never
B 型の値のプロパティを読むと、その理由が報告されます: Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents.。自分で作った型が never になったら、互いに矛盾する2つの部分を探しましょう。
never、unknown、any
| 型 | 持つ値 | 代入できる先 | 受け付けるもの |
|---|---|---|---|
unknown | すべての値 | unknown と any だけ | すべて |
any | すべての値 | never 以外のすべて | すべて |
never | 値なし | すべて | never だけ |
unknown は型の階層のいちばん上、never はいちばん下です。any はそもそも階層の一部ではなく、チェックを止めるものです。
よくある質問
TypeScriptの never 型とは何ですか?
never は値を1つも持たない型です。何も代入できず(別の never を除く)、あらゆる型に代入できます。常に例外を投げる関数や無限ループする関数の戻り値の型として、絞り込みですべての可能性が除外されたあとの変数の型として、string & number のようなありえない型の結果として現れます。
never と void の違いは何ですか?
void を返す関数は普通に終わり、役に立つ値を返さないだけです(実行時には undefined を返します)。never を返す関数はそもそも終わりません。例外を投げるか、永遠に動き続けます。never の関数を呼んだあとのコードには到達できず、TypeScriptは絞り込みのときにそのように扱います。
TypeScriptで網羅性チェックをするには?
ユニオン型に対する switch の default ブランチで、値を never 型の変数に代入するか、例外を投げる関数 assertNever(value: never): never に渡します。すべてのケースを処理していれば、そこでの値は never なのでコンパイルできます。ケースが足りなければ、足りないメンバーは never に代入できないとコンパイラーが報告します。
型が never になってしまうのはなぜですか?
たいていは、TypeScriptがすべての選択肢を絞り込みで除外した(たとえば string | number に対して typeof x === "string" と typeof x === "number" をチェックしたあと)か、交差型がありえないものになった(string & number、または共通のプロパティのリテラル型が矛盾する2つのオブジェクト型)からです。エディターで型にカーソルを合わせると、どの段階で never になったかがわかります。
「is not assignable to type never」とはどういう意味ですか?
never しか許されない場所に実際の値を入れようとしたという意味です。網羅性チェックでは、ユニオン型のメンバーが処理されていないことを表します。それ以外の場所では、配列が never[] と推論されたか、交差型が never に潰れたことが多いです。型注釈を付けるか、矛盾する型を直しましょう。