概要: このチュートリアルでは、決して発生しない値を表す TypeScript の never
型について学びます。
TypeScript の never 型の紹介
TypeScript では、型は値の集合のようなものです。たとえば、number
型は 1、2、3 などの数値を保持します。string
型は、'Hi'
、'Hello'
などの文字列を保持します。null
型は、null
という単一の値を保持します。
never
型は、値を保持しない型です。空集合のようなものです。
never
型は値を保持しないため、never
型の変数に値を代入することはできません。
たとえば、以下はエラーになります。
let empty: never = 'hello';
Code language: JavaScript (javascript)
TypeScript コンパイラは次のエラーを発行します。
Type 'string' is not assignable to type 'never'
Code language: JavaScript (javascript)
では、そもそもなぜ never
型が必要なのでしょうか?
never
型は値がゼロであるため、型システムにおいて不可能を示すために使用できます。
たとえば、文字列と数値の両方を同時に持つことができるインターセクション型を持っているとします。これは不可能です。
type Alphanumeric = string & number; // never
Code language: JavaScript (javascript)
したがって、TypeScript コンパイラは Alphanumeric
の型を never
と推論します。
これは、string
と number
が相互に排他的であるためです。つまり、値は string
と number
の両方を同時に持つことはできません。
通常、never
型は、呼び出し元に制御を返さない関数の戻り値の型を表すために使用します。たとえば、常にエラーをスローする関数などです。
function raiseError(message: string): never {
throw new Error(message);
}
Code language: TypeScript (typescript)
void を返すが、それでも呼び出し元に制御を返す関数と混同しないでください。
無限ループを含む関数がある場合、その戻り値の型は never
にする必要があります。例:
function forever(): never {
while (true) {}
}
Code language: TypeScript (typescript)
この例では、forever()
関数の戻り値の型は never
です。
TypeScript の never 型の例
never
型を使用する例を見てみましょう。
type Role = 'admin' | 'user';
const authorize = (role: Role): string => {
switch (role) {
case 'admin':
return 'You can do anything';
case 'user':
return 'You can do something';
default:
// never reach here util we add a new role
const _unreachable: never = role;
throw new Error(`Invalid role: ${_unreachable}`);
}
};
console.log(authorize('admin'));
Code language: JavaScript (javascript)
仕組み
ステップ 1. 文字列 'admin'
または 'user'
のいずれかである Role
型を定義します。
type Role = 'admin' | 'user';
Code language: JavaScript (javascript)
ステップ 2. Role
型の値を受け取り、文字列を返す authorize()
関数を作成します。
const authorize = (role: Role): string => {
switch (role) {
case 'admin':
return 'You can do anything';
case 'user':
return 'You can do something';
default:
// never reach here util we add a new role
const _unreachable: never = role;
throw new Error(`Invalid role: ${_unreachable}`);
}
};
Code language: JavaScript (javascript)
仕組み
まず、switch ステートメントを使用して、役割が admin
または user
の場合に、対応する文字列を返します。
次に、never
型の _unreachable
という変数を定義し、それに role
を代入します。また、実行が default
ブランチに到達することはないため、default
ブランチでエラーをスローします。
なぜ default
ケースを処理するのでしょうか?
理由は、Role
型に新しい値を追加し、新しい役割を処理するロジックを追加するのを忘れた場合、TypeScript がエラーを発行するためです。
type Role = 'admin' | 'user' | 'guest';
Code language: JavaScript (javascript)
この場合、Role
型に 'guest'
を追加します。
すると、TypeScript は次のエラーを発行します。
Type 'string' is not assignable to type 'never'.ts(2322)
Code language: JavaScript (javascript)
これは、default
ブランチの役割の値が文字列 'guest'
になり、never
型の変数に文字列値を代入することができないためです。
これを修正するには、新しい役割を処理するための新しい case
ブランチを作成する必要があります。
const authorize = (role: Role): string => {
switch (role) {
case 'admin':
return 'You can do anything';
case 'user':
return 'You can do something';
case 'guest':
return 'You can do nothing';
default:
// never reach here util we add a new role
const _unreachable: never = role;
throw new Error(`Invalid role: ${_unreachable}`);
}
};
Code language: JavaScript (javascript)
さらに簡潔にするために、戻り値の型が never の関数を定義し、default
ブランチで使用できます。
type Role = 'admin' | 'user' | 'guest';
const unknownRole = (role: never): never => {
throw new Error(`Invalid role: ${role}`);
};
const authorize = (role: Role): string => {
switch (role) {
case 'admin':
return 'You can do anything';
case 'user':
return 'You can do something';
case 'guest':
return 'You can do nothing';
default:
// never reach here util we add a new role
return unknownRole(role);
}
};
console.log(authorize('admin'));
Code language: TypeScript (typescript)
まとめ
- 値を保持しない
never
型を使用し、型システムにおける不可能を示します。