TypeScript の never 型

概要: このチュートリアルでは、決して発生しない値を表す 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; // neverCode language: JavaScript (javascript)

したがって、TypeScript コンパイラは Alphanumeric の型を never と推論します。

これは、stringnumber が相互に排他的であるためです。つまり、値は stringnumber の両方を同時に持つことはできません。

通常、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 型を使用し、型システムにおける不可能を示します。
このチュートリアルは役に立ちましたか?