never型
never
型は「値を持たない」を意味するTypeScriptの特別な型です。
neverの特性
何も代入できない
never
型には何も代入できません。
ts
constType 'number' is not assignable to type 'never'.2322Type 'number' is not assignable to type 'never'.: never = 1; foo
ts
constType 'number' is not assignable to type 'never'.2322Type 'number' is not assignable to type 'never'.: never = 1; foo
たとえany
型でも代入は不可能です。
ts
constany : any = 1;constType 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.: never = foo any ;
ts
constany : any = 1;constType 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.: never = foo any ;
唯一never
型は代入できます。
ts
constfoo : never = 1 as never;
ts
constfoo : never = 1 as never;
何にでも代入できる
never
型はどんな型にも代入できます。
ts
constnev = 1 as never;consta : string =nev ; // 代入OKconstb : string[] =nev ; // 代入OK
ts
constnev = 1 as never;consta : string =nev ; // 代入OKconstb : string[] =nev ; // 代入OK
値が無いとは
never
型の「値が無い」とはどういうことでしょうか。たとえば、例外が必ず発生する関数の戻り値です。戻り値は絶対に取れません。そのため、戻り値の型はnever
型になります。
ts
functionthrowError (): never {throw newError ();}
ts
functionthrowError (): never {throw newError ();}
終了しない関数も戻り値がnever
型になります。
ts
functionforever (): never {while (true) {} // 無限ループ}
ts
functionforever (): never {while (true) {} // 無限ループ}
作り得ない値もnever
型になります。たとえば、number型とstring型の両方に代入可能な値は作れません。そのため、number型とstring型のインターセクション型はnever
型になります。
ts
typeNumberString = number & string;
ts
typeNumberString = number & string;
void型とnever型の違い
void
型はundefined
が代入できますが、never
は値を持てません。
ts
constok : void =undefined ;constType 'undefined' is not assignable to type 'never'.2322Type 'undefined' is not assignable to type 'never'.: never = ng undefined ;
ts
constok : void =undefined ;constType 'undefined' is not assignable to type 'never'.2322Type 'undefined' is not assignable to type 'never'.: never = ng undefined ;
意味的に戻り値でのvoid
とnever
は、戻り値が無い点は同じです。関数が終了するかが異なります。void
は関数が最後まで実行されるという意味です。never
は関数の処理が中断、もしくは、永遠に続くことを意味します。
型 | 戻り値 | 終了するか |
---|---|---|
void | 無い | return されるか、最後まで実行される |
never | 無い | 中断されるか、永遠に実行される |
そのため、戻り値がnever
の関数が最後まで到達できてしまう実装の場合、TypeScriptはコンパイルエラーを出します。
ts
functionA function returning 'never' cannot have a reachable end point.2534A function returning 'never' cannot have a reachable end point.func ():never {}
ts
functionA function returning 'never' cannot have a reachable end point.2534A function returning 'never' cannot have a reachable end point.func ():never {}
neverを使った網羅性チェック
never
の何も代入できないという特性は、網羅性チェック(exhaustiveness check)に応用できます。網羅性チェックとは、ユニオン型を分岐処理するとき、ロジックがすべてのパターンを網羅しているかをコンパイラにチェックさせることを言います。
たとえば、3パターンのユニオン型があるとします。
ts
typeExtension = "js" | "ts" | "json";
ts
typeExtension = "js" | "ts" | "json";
このうち2パターンにだけ対応した分岐処理が次です。これには網羅性がありませんが、TypeScriptは警告を出しません。
網羅性がない分岐ts
functionprintLang (ext :Extension ): void {switch (ext ) {case "js":console .log ("JavaScript");break;case "ts":console .log ("TypeScript");break;// "json"に対する分岐がない}}
網羅性がない分岐ts
functionprintLang (ext :Extension ): void {switch (ext ) {case "js":console .log ("JavaScript");break;case "ts":console .log ("TypeScript");break;// "json"に対する分岐がない}}
網羅性チェックの基本
網羅性チェックを行うには、default
分岐で網羅性をチェックしたい値をnever型に代入します。すると、TypeScriptが代入エラーの警告を出すようになります。
網羅性チェックがついた分岐ts
functionprintLang (ext :Extension ): void {switch (ext ) {case "js":console .log ("JavaScript");break;case "ts":console .log ("TypeScript");break;default:constType 'string' is not assignable to type 'never'.2322Type 'string' is not assignable to type 'never'.: never = exhaustivenessCheck ext ;break;}}
網羅性チェックがついた分岐ts
functionprintLang (ext :Extension ): void {switch (ext ) {case "js":console .log ("JavaScript");break;case "ts":console .log ("TypeScript");break;default:constType 'string' is not assignable to type 'never'.2322Type 'string' is not assignable to type 'never'.: never = exhaustivenessCheck ext ;break;}}
例外による網羅性チェック
一歩進んで網羅性チェック用の例外クラスを定義するのがお勧めです。このクラスは、コンストラクタ引数にnever
型を取る設計にします。
網羅性チェック関数ts
classExhaustiveError extendsError {constructor(value : never,message = `Unsupported type: ${value }`) {super(message );}}
網羅性チェック関数ts
classExhaustiveError extendsError {constructor(value : never,message = `Unsupported type: ${value }`) {super(message );}}
この例外をdefault
分岐で投げるようにします。コンストラクタに網羅性をチェックしたい引数を渡します。こうしておくと、網羅性が満たされていない場合、TypeScriptが代入エラーを警告します。
ts
functionprintLang (ext :Extension ): void {switch (ext ) {case "js":console .log ("JavaScript");break;case "ts":console .log ("TypeScript");break;default:throw newArgument of type 'string' is not assignable to parameter of type 'never'.2345Argument of type 'string' is not assignable to parameter of type 'never'.ExhaustiveError (); ext }}
ts
functionprintLang (ext :Extension ): void {switch (ext ) {case "js":console .log ("JavaScript");break;case "ts":console .log ("TypeScript");break;default:throw newArgument of type 'string' is not assignable to parameter of type 'never'.2345Argument of type 'string' is not assignable to parameter of type 'never'.ExhaustiveError (); ext }}
例外にしておく利点は2つあります。
noUnusedLocals
に対応可能- 実行時を意識したコードになる
noUnusedLocals
に対応可能
コンパイラオプションnoUnusedLocals
は使われていない変数について警告を出すかを設定します。これがtrue
のとき、変数に代入するだけの網羅性チェックはコンパイルエラーになります。
全網羅するも未使用変数で警告されるts
functionfunc (value : "yes" | "no"): void {switch (value ) {case "yes":console .log ("YES");break;case "no":console .log ("NO");break;default:const'exhaustivenessCheck' is declared but its value is never read.6133'exhaustivenessCheck' is declared but its value is never read.: never = exhaustivenessCheck value ;break;}}
全網羅するも未使用変数で警告されるts
functionfunc (value : "yes" | "no"): void {switch (value ) {case "yes":console .log ("YES");break;case "no":console .log ("NO");break;default:const'exhaustivenessCheck' is declared but its value is never read.6133'exhaustivenessCheck' is declared but its value is never read.: never = exhaustivenessCheck value ;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』より