「発生し得ない値」などのように説明されるnever
型。
概念としては分かるのだが、実際にどのようなケースで使えばよいのかイメージできずにいた。
never
を使ったテクニックを調べていて多少のイメージは掴めてきたので、整理しておく。
動作確認は TypeScript のv3.7.5
で行っている。
never 型の特性
まずはnever
型がどういった型なのか、理解する。
決して発生し得ない値や型は、never
型になる。
例えば以下のif
文では、else
ブロックは絶対に実行されないため、そのなかではfoo
はnever
になる。
const foo = true; if (foo) { foo; // const foo: true } else { foo; // const foo: never }
「存在し得ない値」なので、どんな値も代入することはできない。
const foo: never = 1; // Error const bar: never = undefined; // Error const baz: never = null; // Error
そして、存在し得ない型であるという特性上、T | never
は必ずT
になる。
type Foo = number | never; // type Foo = number type Bar<T> = T | never; type Baz = Bar<number>; // type Baz = number type Qux = Bar<string>; // type Qux = string
存在し得ない型を Union Types に追加しても何の変化も生まれないため、このような結果になる。
type Foo = 1 | 2; // type Foo = 1 | 2 type Bar = 1 | 2 | 3; // type Bar = 1 | 2 | 3 type Baz = 1 | 2 | 3 | never; // type Baz = 1 | 2 | 3
以上の特性を利用したテクニックを 2 つ挙げる。
Type Guard と組み合わせる
never
と Type Guard を組み合わせることで、組み合わせなかった場合には見逃してしまうエラーを、TypeScript が検知できるようになる。
例としてSignal
型とgetMessageFromSignal
関数を実装する。
まずはnever
を使わずに書いた場合。
type Signal = 'green' | 'red'; const getMessageFromSignal = (signal: Signal): string => { switch (signal) { case 'green': { return 'Go!'; } case 'red': { return 'Stop!'; } default: { throw new Error(`${signal} is not Signal.`); } } };
getMessageFromSignal
にSignal
を渡し、その値に応じて文字列を返す。
引数にSignal
型以外の値を渡すと、TypeScript はエラーを吐く。
そのため例えばgrean
のようにタイプミスしても、それに気付くことができる。
console.log(getMessageFromSignal('green')); // Go! console.log(getMessageFromSignal('grean')); // Argument of type '"grean"' is not assignable to parameter of type 'Signal'.
特に問題ないように見えるが、Signal
の値を増やしたときにgetMessageFromSignal
側の対応を怠ると、TypeScript では検知できないエラーが発生してしまう。
以下ではSignal
にyellow
を追加したが、getMessageFromSignal
側ではその対応を行っていない。
そのためgetMessageFromSignal
にyellow
を渡すと、default
ブロックが実行されてエラーが投げられる。
type Signal = 'green' | 'red' | 'yellow'; const getMessageFromSignal = (signal: Signal): string => { switch (signal) { case 'green': { return 'Go!'; } case 'red': { return 'Stop!'; } default: { throw new Error(`${signal} is not Signal.`); } } };
だがyellow
はSignal
型なので、getMessageFromSignal
に渡しても TypeScript はエラーを出してくれない。
そのため、このコードを実際に実行したときに初めてエラーに気付くことになる。
// 型チェックを行ってもエラーにならない console.log(getMessageFromSignal('yellow')); // Error: yellow is not Signal.
never
型を使うことで、このエラーを TypeScript が検知してくれるようになる。
具体的には、default
ブロックのなかで明示的にnever
を使う。
type Signal = 'green' | 'red' | 'yellow'; const getMessageFromSignal = (signal: Signal): string => { switch (signal) { case 'green': { return 'Go!'; } case 'red': { return 'Stop!'; } default: { const strangeValue: never = signal; // Type '"yellow"' is not assignable to type 'never'.ts(2322) throw new Error(`${strangeValue} is not Signal.`); } } };
Type Guard によって型の絞り込みが行われていくため、default
ブロックのなかのsignal
はgreen
でもred
でもない値、つまりyellow
になっている。
にも関わらずnever
型に代入しようとしているため、TypeScript はエラーを吐く。そのため、コードの実行前にgetMessageFromSignal
の考慮漏れに気付くことができる。
yellow
ケースを追加すればdefault
ブロック内のsignal
はnever
となるため、エラーは消える。
type Signal = 'green' | 'red' | 'yellow'; const getMessageFromSignal = (signal: Signal): string => { switch (signal) { case 'green': { return 'Go!'; } case 'red': { return 'Stop!'; } case 'yellow': { return 'Warning!'; } default: { const strangeValue: never = signal; throw new Error(`${strangeValue} is not Signal.`); } } }; console.log(getMessageFromSignal('yellow')); // Warning!
以降、Signal
が追加される度にエラーが出るため、getMessageFromSignal
の考慮漏れを見落とすことがなくなる。
Conditional Types と組み合わせる
never
は Conditional Types と組み合わせることで、型の絞り込みに利用できる。
Conditional Types に対して Union Types を使うと、Union Types のそれぞれの型に対して Conditional Types が展開される。
以下の例だとgreen
とblack
それぞれに対してIsSignal
が実行される。
type Signal = 'green' | 'red' | 'yellow'; type IsSignal<T> = T extends Signal ? true : false; type Foo = IsSignal<'green' | 'black'>; // type Foo = boolean
以下が、IsSignal<'green' | 'black'>
がboolean
になるまでの流れ。
IsSignal<'green' | 'black'> ↓ ('green' extends Signal ? true : false) | ('black' extends Signal ? true : false) ↓ true | false ↓ boolean
この性質をnever
と組み合わせると、既存の型から特定の型をフィルタリングすることができる。
以下のSignalFilter
は、Signal
が渡されたときはそれをそのまま返し、Signal
以外を渡された場合はnever
を返す。
これを使うことで、Signal
以外の値を取り除くことができる。
type Signal = 'green' | 'red' | 'yellow'; type SignalFilter<T> = T extends Signal ? T : never; type Foo = SignalFilter<'green' | 'orange' | 'black' | 'yellow'>; // type Foo = "green" | "yellow"
上記のFoo
の場合、"green" | never | never | "yellow"
となるが、Union Types のなかのnever
は無視されるため、"green" | "yellow"
となる。
このテクニックは TypeScript 本体でも使われており、NonNullable
は以下の実装になっている。
type NonNullable<T> = T extends null | undefined ? never : T;
null
とundefined
はnever
を返すため、それ以外の値が残る。
type Foo = NonNullable<null | 0 | 1>; // type Foo = 0 | 1 type Bar = NonNullable<null | '' | undefined | true>; // type Bar = "" | true