Tackling TypeScript
本書をサポートしてください: 購入または寄付
(広告、ブロックしないでください。)

12 TypeScript の列挙型: 仕組みと利用方法



この章では、次の 2 つの質問に答えます。

次の章では、列挙型の代替案について検討します。

12.1 基本事項

boolean は有限の値を持つ型です: falsetrue です。TypeScript では、列挙型を使用して同様の型を自分で定義できます。

12.1.1 数値列挙型

これは数値列挙型です。

enum NoYes {
  No = 0,
  Yes = 1, // trailing comma
}

assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);

説明

メンバーは true123、または 'abc' などのリテラルとして使用できます。たとえば

function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}
assert.equal(toGerman(NoYes.No), 'Nein');
assert.equal(toGerman(NoYes.Yes), 'Ja');

12.1.2 文字列ベースの列挙型

数値の代わりに、列挙型メンバー値として文字列を使用することもできます。

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');

12.1.3 異種列挙型

最後の列挙型の種類は異種と呼ばれます。異種列挙型のメンバー値は、数値と文字列が混在しています。

enum Enum {
  One = 'One',
  Two = 'Two',
  Three = 3,
  Four = 4,
}
assert.deepEqual(
  [Enum.One, Enum.Two, Enum.Three, Enum.Four],
  ['One', 'Two', 3, 4]
);

異種の列挙型は用途が少ないため、あまり使用されません。

また、TypeScriptでは列挙型メンバー値として数値と文字列しかサポートしていません。その他の値(シンボルなど)は許可されません。

12.1.4 初期化子の省略

次の2つのケースでは初期化子を省略できます。

初期化子を持たない数値列挙型の例を示します。

enum NoYes {
  No,
  Yes,
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);

初期化子が省略されている異種の列挙型の例を示します。

enum Enum {
  A,
  B,
  C = 'C',
  D = 'D',
  E = 8, // (A)
  F,
}
assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0, 1, 'C', 'D', 8, 9]
);

行Aの初期化子を省略できないことに注意してください。これは、前のメンバーの値が数値ではないためです。

12.1.5 列挙型メンバー名のケース

定数(列挙型またはその他の場所)の命名にはいくつかの規範があります。

12.1.6 列挙型メンバー名の引用符付与

JavaScriptオブジェクトと同様に、列挙型メンバーの名前に引用符を付けることができます。

enum HttpRequestField {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}
assert.equal(HttpRequestField['Accept-Charset'], 1);

列挙型メンバーの名前を計算する方法はありません。 オブジェクトリテラルでは、角かっこを使用して計算されたプロパティキーがサポートされます。

12.2 列挙型メンバー値の指定(高度な内容)

TypeScriptでは、列挙型メンバーを初期化する方法によって3つの種類に分類されます。

これまでにリテラルメンバーのみを使用してきました。

前のリストで、早くに言及されているメンバーは柔軟性が低くなりますが、より多くの機能をサポートします。 詳細については読み続けてください。

12.2.1 リテラル列挙メンバー

列挙型メンバーはその値が指定されている場合、リテラルです。

列挙型にリテラルメンバーのみがある場合、それらのメンバーを型として使用できます(たとえば、数値リテラルを型として使用できる方法と同様です)。

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(x: NoYes.No) { // (A)
  return x;
}

func(NoYes.No); // OK

// @ts-expect-error: Argument of type '"No"' is not assignable to
// parameter of type 'NoYes.No'.
func('No');

// @ts-expect-error: Argument of type 'NoYes.Yes' is not assignable to
// parameter of type 'NoYes.No'.
func(NoYes.Yes);

行AのNoYes.No列挙型メンバー型です。

さらに、リテラル列挙型は完全性チェックをサポートします(後述します)。

12.2.2 定数列挙メンバー

列挙型メンバーは、その値をコンパイル時に計算できる場合、定数です。 したがって、値を暗黙的に指定するか(つまり、TypeScriptに指定させます)。または、明示的に指定し、次の構文のみを使用できます。

メンバーがすべて定数の列挙型の例を示します(後述で列挙型がどのように使用されるかを参照します)。

enum Perm {
  UserRead     = 1 << 8, // bit 8
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}

一般に、定数メンバーを型として使用することはできません。 ただし、完全性チェックは引き続き実行されます。

12.2.3 計算列挙メンバー

計算列挙メンバーの値は、任意の式で指定できます。 たとえば

enum NoYesNum {
  No = 123,
  Yes = Math.random(), // OK
}

これは数値列挙型でした。文字列ベース列挙型と異種列挙型はさらに制限されます。たとえば、メソッド呼び出しを使用してメンバー値を指定することはできません。

enum NoYesStr {
  No = 'No',
  // @ts-expect-error: Computed values are not permitted in
  // an enum with string valued members.
  Yes = ['Y', 'e', 's'].join(''),
}

TypeScript は、計算された列挙型メンバーの網羅性チェックを行いません。

12.3 数値列挙型の短所

12.3.1 短所: ロギング

数値列挙型のメンバーをログに記録すると、数字のみが表示されます。

enum NoYes { No, Yes }

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 0
// 1

12.3.2 短所: 緩い型のチェック

列挙型を型として使用すると、静的に許可される値は列挙型メンバーの値だけではなく、任意の数字が許可されます。

enum NoYes { No, Yes }
function func(noYes: NoYes) {}
func(33); // no error!

なぜ、より厳密な静的チェックが行われないのでしょうか? Daniel Rosenwasser が説明しています。

動作はビット単位演算に基づいています。SomeFlag.Foo | SomeFlag.Bar が別の SomeFlag を生成することを意図している場合があります。その代わりに number が生成され、SomeFlag にキャストバックする必要はありません。

TypeScript をもう一度やり直して列挙型が残っているとしたら、ビットフラグ用の別個の構文を作ると思います。

列挙型がビットパターンに使用される方法は、もう少し詳しく後程示します。

12.3.3 推奨事項: 文字列ベースの列挙型を使用

私の推奨事項は、文字列ベースの列挙型を使用することです (簡潔にするため、この章では常にこの推奨事項に従っているわけではありません)。

enum NoYes { No='No', Yes='Yes' }

一方で、ログ出力は人間にとってより役立ちます。

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 'No'
// 'Yes'

一方で、より厳密な型チェックが得られます。

function func(noYes: NoYes) {}

// @ts-expect-error: Argument of type '"abc"' is not assignable
// to parameter of type 'NoYes'.
func('abc');

// @ts-expect-error: Argument of type '"Yes"' is not assignable
// to parameter of type 'NoYes'.
func('Yes'); // (A)

メンバーの値と等しい文字列でさえ許可されません (A 行)。

12.4 列挙型のユースケース

12.4.1 ユースケース: ビットパターン

Node.js のファイルシステムモジュール では、いくつかの関数が mode パラメータを持ちます。それは、Unix から残った数値エンコーディングによるファイルのパーミッションを指定します。

つまり、パーミッションは9ビット (各3つのパーミッションを持つ3つのカテゴリ) で表現できます。

ユーザー グループ すべて
パーミッション r, w, x r, w, x r, w, x
ビット 8, 7, 6 5, 4, 3 2, 1, 0

Node.js はこれを実行しませんが、列挙型を使用してこれらのフラグを扱うことができます。

enum Perm {
  UserRead     = 1 << 8, // bit 8
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}

ビットパターンは ビット単位 OR を通じて組み合わせられます。

// User can change, read and execute.
// Everyone else can only read and execute.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.UserExecute |
  Perm.GroupRead | Perm.GroupExecute |
  Perm.AllRead | Perm.AllExecute,
  0o755);

// User can read and write.
// Group members can read.
// Everyone can’t access at all.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.GroupRead,
  0o640);
12.4.1.1 ビットパターンの代替案

ビットパターンの背後にある主なアイデアは、一連のフラグがあり、それらのフラグの任意のサブセットを選択できるというものです。

したがって、実際のセットを使用してサブセットを選択することは、同じタスクを実行するためのより簡単な方法です。

enum Perm {
  UserRead = 'UserRead',
  UserWrite = 'UserWrite',
  UserExecute = 'UserExecute',
  GroupRead = 'GroupRead',
  GroupWrite = 'GroupWrite',
  GroupExecute = 'GroupExecute',
  AllRead = 'AllRead',
  AllWrite = 'AllWrite',
  AllExecute = 'AllExecute',
}
function writeFileSync(
  thePath: string, permissions: Set<Perm>, content: string) {
  // ···
}
writeFileSync(
  '/tmp/hello.txt',
  new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
  'Hello!');

12.4.2 ユースケース: 複数の定数

時には、一緒に属する定数のセットがあります。

const off = Symbol('off');
const info = Symbol('info');
const warn = Symbol('warn');
const error = Symbol('error');

これは列挙型の良いユースケースです。

enum LogLevel {
  off = 'off',
  info = 'info',
  warn = 'warn',
  error = 'error',
}

列挙型の利点の 1 つは、定数名がグループ化され、LogLevel 名前空間内で入れ子になっていることです。

もう 1 つの利点は、それらに対して LogLevel 型を自動的に取得することです。定数に対してそのような型が必要な場合は、さらに作業が必要です。

type LogLevel =
  | typeof off
  | typeof info
  | typeof warn
  | typeof error
;

このアプローチの詳細については、§13.1.3 「シンボルシントングループの共役」 を参照してください。

12.4.3 ユースケース: ブール値よりも自己記述的

選択肢を表すためにブール値が使用されている場合、列挙型がより自己記述的です。

12.4.3.1 真偽値の例: 順序付きリストと非順序付きリスト

たとえば、リストが順序付きであるかどうかを表すには、真偽値を使用できます

class List1 {
  isOrdered: boolean;
  // ···
}

ただし、列挙型は自己記述的で、必要に応じて後から選択肢を追加できるという追加の利点があります。

enum ListKind { ordered, unordered }
class List2 {
  listKind: ListKind;
  // ···
}
12.4.3.2 真偽値の例: エラー処理モード

同様に、真偽値を使用してエラー処理方法を指定できます

function convertToHtml1(markdown: string, throwOnError: boolean) {
  // ···
}

または、列挙値を使用してこれを行うことができます

enum ErrorHandling {
  throwOnError = 'throwOnError',
  showErrorsInContent = 'showErrorsInContent',
}
function convertToHtml2(markdown: string, errorHandling: ErrorHandling) {
  // ···
}

12.4.4 ユースケース: 優れた文字列定数

正規表現を作成する次の関数を検討してください。

const GLOBAL = 'g';
const NOT_GLOBAL = '';
type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;

function createRegExp(source: string,
  globalness: Globalness = NOT_GLOBAL) {
    return new RegExp(source, 'u' + globalness);
  }

assert.deepEqual(
  createRegExp('abc', GLOBAL),
  /abc/ug);

assert.deepEqual(
  createRegExp('abc', 'g'), // OK
  /abc/ug);

文字列定数の代わりに列挙型を使用できます

enum Globalness {
  Global = 'g',
  notGlobal = '',
}

function createRegExp(source: string, globalness = Globalness.notGlobal) {
  return new RegExp(source, 'u' + globalness);
}

assert.deepEqual(
  createRegExp('abc', Globalness.Global),
  /abc/ug);

assert.deepEqual(
  // @ts-expect-error: Argument of type '"g"' is not assignable to parameter of type 'Globalness | undefined'. (2345)
  createRegExp('abc', 'g'), // error
  /abc/ug);

このアプローチの利点は何ですか?

12.5 実行時の列挙型

TypeScript は、列挙型を JavaScript オブジェクトにコンパイルします。たとえば、次の列挙型を考えてみます

enum NoYes {
  No,
  Yes,
}

TypeScript はこの列挙型を次のようにコンパイルします

var NoYes;
(function (NoYes) {
  NoYes[NoYes["No"] = 0] = "No";
  NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));

このコードでは、次の割り当てが行われます

NoYes["No"] = 0;
NoYes["Yes"] = 1;

NoYes[0] = "No";
NoYes[1] = "Yes";

割り当てには 2 つのグループがあります

12.5.1 逆マッピング

数値列挙型の場合

enum NoYes {
  No,
  Yes,
}

通常のマッピングは、メンバー名からメンバー値です

// Static (= fixed) lookup:
assert.equal(NoYes.Yes, 1);

// Dynamic lookup:
assert.equal(NoYes['Yes'], 1);

数値列挙型は、メンバー値からメンバー名への逆マッピングもサポートしています

assert.equal(NoYes[1], 'Yes');

逆マッピングの 1 つのユースケースは、列挙型のメンバーの名前に基づいて名前を出力することです

function getQualifiedName(value: NoYes) {
  return 'NoYes.' + NoYes[value];
}
assert.equal(
  getQualifiedName(NoYes.Yes), 'NoYes.Yes');

12.5.2 実行時の文字列ベースの列挙型

文字列ベースの列挙型は、実行時の表現がより単純です。

次の列挙型を検討してください。

enum NoYes {
  No = 'NO!',
  Yes = 'YES!',
}

この場合、JavaScript コードはこのようにコンパイルされます

var NoYes;
(function (NoYes) {
    NoYes["No"] = "NO!";
    NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));

TypeScript は、文字列ベースの列挙型では逆マッピングをサポートしません。

12.6 const 列挙型

const キーワードが列挙型の前に付けられている場合、実行時の表現がありません。代わりに、メンバーの値が直接使用されます。

12.6.1 非 const 列挙型のコンパイル

この効果を観察するには、最初に次の非 const 列挙型を検討してみましょう

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

TypeScript は、このコードを次のようにコンパイルします

"use strict";
var NoYes;
(function (NoYes) {
  NoYes["No"] = "No";
  NoYes["Yes"] = "Yes";
})(NoYes || (NoYes = {}));

function toGerman(value) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

12.6.2 const 列挙型のコンパイル

これは以前と同じコードですが、今度は列挙型は const です

const enum NoYes {
  No,
  Yes,
}
function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

これで、列挙型の構成は構造として消滅し、メンバーの値のみが残ります

function toGerman(value) {
  switch (value) {
    case "No" /* No */:
      return 'Nein';
    case "Yes" /* Yes */:
      return 'Ja';
  }
}

12.7 コンパイル時の列挙型

12.7.1 列挙型はオブジェクトです

TypeScript は、(非 const)列挙型をオブジェクトのように扱います

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(obj: { No: string }) {
  return obj.No;
}
assert.equal(
  func(NoYes), // allowed statically!
  'No');

12.7.2 リテラル列挙型の安全性チェック

列挙型のメンバー値を受け入れる場合、次のことを確認することがよくあります

詳細は、以下をお読みください。次の列挙型を使用します

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
12.7.2.1 不正な値から保護する

次のコードでは、不正な値に対して 2 つの措置を講じています

function toGerman1(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
    default:
      throw new TypeError('Unsupported value: ' + JSON.stringify(value));
  }
}

assert.throws(
  // @ts-expect-error: Argument of type '"Maybe"' is not assignable to
  // parameter of type 'NoYes'.
  () => toGerman1('Maybe'),
  /^TypeError: Unsupported value: "Maybe"$/);

措置は次のとおりです

12.7.2.2 網羅性チェックによるケースの考慮忘れを防ぐ

さらに1つの対策があります。次のコードでは網羅性チェックが実行されます。列挙メンバーをすべて考慮し忘れた場合、TypeScriptから警告されます。

class UnsupportedValueError extends Error {
  constructor(value: never) {
    super('Unsupported value: ' + value);
  }
}

function toGerman2(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
    default:
      throw new UnsupportedValueError(value);
  }
}

網羅性チェックはどのように機能しますか?各ケースで、TypeScriptはvalueの型を推論します。

function toGerman2b(value: NoYes) {
  switch (value) {
    case NoYes.No:
      // %inferred-type: NoYes.No
      value;
      return 'Nein';
    case NoYes.Yes:
      // %inferred-type: NoYes.Yes
      value;
      return 'Ja';
    default:
      // %inferred-type: never
      value;
      throw new UnsupportedValueError(value);
  }
}

既定のケースで、TypeScriptはvalueの型をneverと推論します。なぜなら、そこにたどり着かないからです。しかしながら、NoYesにメンバー.Maybeを追加した場合、valueの推論型はNoYes.Maybeになります。そして、その型はnew UnsupportedValueError()のパラメータのneverという型と静的に非互換です。コンパイル時に以下のエラーメッセージが表示されるのはそのためです。

Argument of type 'NoYes.Maybe' is not assignable to parameter of type 'never'.

便利なことに、この種の網羅性チェックはifステートメントでも機能します。

function toGerman3(value: NoYes) {
  if (value === NoYes.No) {
    return 'Nein';
  } else if (value === NoYes.Yes) {
    return 'Ja';
  } else {
    throw new UnsupportedValueError(value);
  }
}
12.7.2.3 網羅性をチェックする別の方法

あるいは、戻り値の型を指定する場合も網羅性チェックを取得できます。

function toGerman4(value: NoYes): string {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return 'Nein';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return 'Ja';
  }
}

NoYesにメンバーを追加した場合、TypeScriptはtoGerman4()undefinedを返す可能性があると文句を言います。

このアプローチの欠点

12.7.3 keyofと列挙型

keyof型演算を使用して、列挙メンバーのキーを要素とする型を作成できます。そうする場合、keyoftypeofと組み合わせる必要があります。

enum HttpRequestKeyEnum {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}
// %inferred-type: "Accept" | "Accept-Charset" | "Accept-Datetime" |
// "Accept-Encoding" | "Accept-Language"
type HttpRequestKey = keyof typeof HttpRequestKeyEnum;

function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
  // ···
}
12.7.3.1 typeofなしでkeyofを使用する

typeofなしでkeyofを使用すると、別のあまり有益ではない型が得られます

// %inferred-type: "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"
type Keys = keyof HttpRequestKeyEnum;

keyof HttpRequestKeyEnumkeyof numberと同じです。

12.8 謝辞