Menu

TypeScriptのnever型: 網羅性チェックとvoidとの違い

never は値を1つも持たない型です。終わらない関数の戻り値の型であり、絞り込みですべてのケースを除外したあとに残る型であり、switch の網羅性チェックを支える道具です。never がどこから現れるか、void とどう違うかを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

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

voidnever
関数が終わるかはいいいえ(例外を投げるか無限ループ)
実行時の値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 に潰れたことが多いです。型注釈を付けるか、矛盾する型を直しましょう。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める