never - TypeScriptキーワード一覧
スポンサーリンク
「never」キーワードは「値が発生しえない型」である「never 型」を表す型として用いられます。この型がコード中に現れている場合、そのコードは到達しない処理であるかエラーなど想定外の状態が起きる(起きている)可能性があります。
TypeScript 2.0 で導入されています。
概要
(特筆の無いものは 「TypeScript PR #8652: Add 'never' type」での説明によります。)
- (型としての)キーワード「never」(または「never 型」)は『値が全く現れることのない型』を示します。
- 「値が全く現れることのない」とは、ある関数の内部処理が無限ループ構造になるなどで「return」されない場合(=undefined 含め戻り値が返ることが無い)、あるいは、ある変数の型ガード処理の結果、(静的解析上)そのガード内に入ることがあり得ない場合、などが含まれます。
- なお、関数(アロー関数も含む)の戻り値が明示的に「never」とされている場合、その関数内で(便宜上の「never 値」を除いて)あらゆる値をreturnすること、および関数の末尾に処理が到達する状態になっていることはエラーと扱われます。
- 「never 型」はすべての型の部分型(subtype)となり、そのデータ(= 便宜上の「never 値」)はすべての変数や引数に代入可能です。(※ 実際には「never 値」は存在しません。)
- 逆に、「never 型」を除くすべての型のデータは「never 型」への代入はできません。(「any 型」の値も代入できません。)
キーワード情報
- 「never」は識別子としては予約されていません。(ユーザー定義の識別子として利用できます。)
- 「never」は型名として予約されていません。(ユーザー定義の型名として利用できます。)
- ただし、ユーザー定義の「never」という型名を定義しても、その空間(名前空間など)を明示的に示すような指定が無い場合、「never」という型名を参照するとTypeScript組み込みの「never 型」が使用されます。(2018年8月時点で仕様に明記されていない模様であり、混乱の元になるので「never」というユーザー定義型を作るのは避けるべきです。)
詳細
キーワードとしての「never」は「never 型」を表すための型名として利用されます。「never 型」は自分自身(「never 型」)のデータ以外を受け付けない(代入不可)という点で特異な型となっています。
let x: never; // never 型変数
x = 634; // ERROR: TS2322: 数値をnever型に代入できない
x = 634 as any; // ERROR: TS2322: any型をnever型に代入できない
console.log(x.toString()); // ERROR: TS2339: never型に「toString」は存在しない
「never 型」はES2015の概念には存在せず、それを表す値(「never 値」)も存在していません。そのことから「never 型」へのデータの代入は実質不可能であるため、変数や引数の型に直接用いることは通常行いませんが、「never 型」は以下のようなケースで使用されます。
- 型推論の結果、取り得る型が無い状態になった時の変数・メンバーに付けられる型
- return文が無く、かつ(無限ループなどで)関数末尾に到達しない関数/アロー関数に対して推論される戻り値の型
- 型の条件分岐(conditional type)にて一方の分岐が想定外(対象外)である場合の型(あるいはunion type(共用体型)からの除去など)
1. 型推論の結果、取り得る型が無い状態になった時の変数・メンバーに付けられる型
「never 型」は「すべての型の部分型(subtype)」という定義であるため、任意の型「T」に対し、それぞれ「T | never
」は「T」と、「T & never
」は「never」と同値(同型)となります。そのため、
let x: string = 'hello'; // string 型変数
if (typeof x === 'string') {
// ここでは x は「string 型」
} else {
// ここでは x は「never 型」扱い
}
における「never 型」の推論については、このコード中の変数「x
」が「string | never
」という型であるために推論されると考えることができます。
※ このケースでは、「x」が「never 型」となっている箇所で「x」を利用しようとすると大抵の場合型チェックでエラーになるため(ならない場合もあります)、どこか実装に誤りがある可能性を考えることができます。
2. return文が無く、かつ(無限ループなどで)関数末尾に到達しない関数/アロー関数に対して推論される戻り値の型
関数/アロー関数の戻り値における「never 型」は、以下のようなコードを考えると「戻り値が生じないのでneverになる」と考えることができるかと思います。
// このアロー関数は無限ループ構造となっており、処理が関数末尾に到達しないので戻り値が「never」となる
const myLoop = () => {
while (true) {
if (!processReadData()) {
process.exit(0);
}
}
};
// この関数は常に例外をスローし、処理が関数末尾に到達しないので戻り値が「never」とできる
// (※ 戻り値を明示しない場合は戻り値の型が void として扱われる)
function throwError(msg: string): never {
throw new Error(msg);
}
戻り値の型が「never 型」となっている関数から戻り値を受け取った場合、(受け取り先で型を明示しなければ)そのデータは「never 型」になるので、やはりその値を利用するようなコードでエラーになることになり、コードの誤りに気付くことができる可能性があります。また、関数の戻り値を明示的に「never」と記述しておけば、その関数内でうっかり「never 型」以外の値をreturnしてしまった場合や、関数の末尾に到達してしまうようなコードになっていた場合にエラーとなるので、そういったミスも未然に防ぐことができます。
※ 「never 型」自体は他の型に代入可能であるため、「never 型」の戻り値の受け取り先が既に決まった型を持っている場合には特にエラーになりません。ただし「『never』である以上その関数が制御を返さない」と考えることができるため、「『never 型』の値を代入」してしまったとしても、後続の処理自体が想定外の挙動になることはないと考えられます。
3. 型の条件分岐(conditional type)にて一方の分岐が想定外(対象外)である場合の型(あるいはunion type(共用体型)からの除去など)
※ 型の条件分岐は TypeScript 2.8 で導入されています。参考: 「TypeScript PR #21316: Conditional types」
型の条件分岐は、ほぼ文字通り、型が一定の条件を満たしたときとそうでないときに型を分ける記法ですが、「満たしたとき」あるいは「そうでないとき」に「never」を用いることがあります。
例えば、「Extract<T, U>
」という型(Generic型)は「T
から U
を抽出する型」として利用されますが、この型はTypeScriptの標準ライブラリファイル(lib.es5.d.ts)で以下のように定義されています。
type Extract<T, U> = T extends U ? T : never;
この Extract
は「T
が U
に代入可能(assignable)」である場合に T
、そうでない場合に「never 型」となる型を示しており、例えば「Extract<string, string>
」は「string
」、「Extract<number, string>
」は「never
」となります。
ここで、型の条件分岐では条件をチェックする型(Extract
における T
)に対して分配則が存在しており、union type(共用体型)およびintersection type(交差型)を指定すると、それぞれ構成する型で分配されて条件分岐が行われます。すなわち、「(X | Y) extends Z ? ...
」は「(X extends Z ? ...) | (Y extends Z ? ...)
」となります。したがって、
let someVar: Extract<'a' | 'b' | 'c' | 'd', 'a' | 'd'>;
// ↓ 分配される
let someVar: Extract<'a', 'a' | 'd'> | Extract<'b', 'a' | 'd'> | Extract<'c', 'a' | 'd'> | Extract<'d', 'a' | 'd'>;
// ↓ それぞれの「Extract」を展開(型 'a' は ('a' | 'd') に代入可能だが型 'b' は ('a' | 'd') に代入できない、などを適用)
let someVar: 'a' | never | never | 'd';
// ↓ 「never 型」は「すべての型の部分型」なので除去される
let someVar: 'a' | 'd';
といった展開が行われるので、「Extract<T, U>
」は「T
から U
を抽出する型」ということを実現しています。この例は「never 型」が「すべての型の部分型である」ことを応用した内容になっていますが、このように型の条件分岐で「never」を用いるケースがあります。
※ 型の条件分岐の結果を「never」とするのは他に、その「never」が割り当たった変数/引数/プロパティー等を用いようとして型チェックエラーを誘発させることで、「その型は指定できない」ことを暗に示す目的でも用いられます。