列挙型

列挙型は、TypeScriptが持つ数少ない機能の1つであり、JavaScriptの型レベルの拡張ではありません。

列挙型を使用すると、開発者は名前付き定数のセットを定義できます。 列挙型を使用すると、意図の文書化や、個別のケースのセットの作成が容易になります。 TypeScriptは、数値ベースと文字列ベースの両方の列挙型を提供します。

数値列挙型

最初に数値列挙型から始めます。これは、他の言語から来ている場合、おそらくより馴染み深いでしょう。 列挙型は、enumキーワードを使用して定義できます。

ts
enum Direction {
Up = 1,
Down,
Left,
Right,
}
Try

上記では、Up1で初期化されている数値列挙型があります。 後続のすべてのメンバーは、その時点から自動的にインクリメントされます。 つまり、Direction.Upの値は1Down2Left3Right4です。

必要に応じて、初期化子を完全に省略できます

ts
enum Direction {
Up,
Down,
Left,
Right,
}
Try

ここで、Upの値は0Down1などになります。 この自動インクリメントの動作は、メンバー値自体には関心がなく、各値が同じ列挙型の他の値と異なることだけに関心がある場合に役立ちます。

列挙型の使用は簡単です。列挙型自体のプロパティとして任意のメンバーにアクセスし、列挙型の名前を使用して型を宣言するだけです

ts
enum UserResponse {
No = 0,
Yes = 1,
}
 
function respond(recipient: string, message: UserResponse): void {
// ...
}
 
respond("Princess Caroline", UserResponse.Yes);
Try

数値列挙型は、計算されたメンバーと定数メンバー(下記参照)に混在させることができます。 簡単に言うと、初期化子がない列挙型は、最初にするか、数値定数または他の定数列挙型メンバーで初期化された数値列挙型の後に配置する必要があります。 つまり、以下は許可されていません

ts
enum E {
A = getSomeValue(),
B,
Enum member must have initializer.1061Enum member must have initializer.
}
Try

文字列列挙型

文字列列挙型も同様の概念ですが、以下に示すように、いくつかの微妙なランタイムの違いがあります。 文字列列挙型では、各メンバーは文字列リテラル、または別の文字列列挙型メンバーで定数初期化する必要があります。

ts
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
Try

文字列列挙型には自動インクリメントの動作はありませんが、文字列列挙型には「シリアライズ」しやすいという利点があります。 つまり、デバッグ中に数値列挙型のランタイム値を読み取る必要がある場合、値は多くの場合不透明です。つまり、それだけでは有用な意味を伝えません(ただし、逆マッピングはしばしば役立ちます)。 文字列列挙型を使用すると、列挙型メンバー自体の名前とは無関係に、コードの実行時に意味のある読み取り可能な値を与えることができます。

異種混合enum

技術的には、enumは文字列と数値のメンバーを混在させることができますが、なぜそうしたいのか明確な理由はありません。

ts
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
Try

JavaScriptのランタイム動作を巧妙に利用しようとしているのでない限り、これを行うことはお勧めしません。

計算型メンバーと定数メンバー

各enumメンバーには、定数または計算型のいずれかである値が関連付けられています。enumメンバーは、以下の場合に定数とみなされます。

  • enumの最初のメンバーであり、初期化子がなく、値0が割り当てられている場合。

    ts
    // E.X is constant:
    enum E {
    X,
    }
    Try
  • 初期化子がなく、前のenumメンバーが数値定数であった場合。この場合、現在のenumメンバーの値は、前のenumメンバーの値に1を加えた値になります。

    ts
    // All enum members in 'E1' and 'E2' are constant.
     
    enum E1 {
    X,
    Y,
    Z,
    }
     
    enum E2 {
    A = 1,
    B,
    C,
    }
    Try
  • enumメンバーが定数enum式で初期化されている場合。定数enum式は、コンパイル時に完全に評価できるTypeScript式のサブセットです。式は、以下の場合に定数enum式となります。

    1. リテラルenum式(基本的には文字列リテラルまたは数値リテラル)
    2. 以前に定義された定数enumメンバーへの参照(別のenumから派生する場合もある)
    3. 括弧で囲まれた定数enum式
    4. 定数enum式に適用される単項演算子+-~のいずれか
    5. オペランドとして定数enum式を持つ二項演算子+-*/%<<>>>>>&|^

    定数enum式がNaNまたはInfinityに評価されるのは、コンパイル時エラーです。

その他すべての場合、enumメンバーは計算型とみなされます。

ts
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length,
}
Try

ユニオンenumとenumメンバー型

計算されない定数enumメンバーの特別なサブセットがあります。リテラルenumメンバーです。リテラルenumメンバーは、初期化されていない値、または以下に初期化された値を持つ定数enumメンバーです。

  • 任意の文字列リテラル(例:"foo""bar""baz"
  • 任意の数値リテラル(例:1100
  • 任意の数値リテラルに適用される単項マイナス(例:-1-100

enumのすべてのメンバーがリテラルenum値を持つ場合、いくつかの特別なセマンティクスが作用します。

1つ目は、enumメンバーも型になることです!たとえば、特定のメンバーがenumメンバーの値のみを持つことができると指定できます。

ts
enum ShapeKind {
Circle,
Square,
}
 
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
 
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
 
let c: Circle = {
kind: ShapeKind.Square,
Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.2322Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
radius: 100,
};
Try

もう1つの変更点は、enum型自体が事実上、各enumメンバーの*ユニオン*になることです。ユニオンenumを使用すると、型システムはenum自体に存在する値の正確なセットを知っているという事実を活用できます。そのため、TypeScriptは値を誤って比較している可能性のあるバグをキャッチできます。例えば

ts
enum E {
Foo,
Bar,
}
 
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.2367This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.
//
}
}
Try

この例では、最初にxE.Foo*ではない*かどうかを確認しました。そのチェックが成功した場合、||はショートサーキットし、「if」の本体が実行されます。ただし、チェックが成功しなかった場合、xは*必ず*E.Fooになるので、E.Barと*等しくない*かどうかを確認するのは意味がありません。

ランタイム時のenum

enumは、ランタイムに存在する実際のオブジェクトです。たとえば、次のenum

ts
enum E {
X,
Y,
Z,
}
Try

は、実際に関数に渡すことができます。

ts
enum E {
X,
Y,
Z,
}
 
function f(obj: { X: number }) {
return obj.X;
}
 
// Works, since 'E' has a property named 'X' which is a number.
f(E);
Try

コンパイル時のenum

enumはランタイムに存在する実際のオブジェクトですが、`keyof`キーワードは一般的なオブジェクトの場合とは異なる動作をします。代わりに、`keyof typeof`を使用して、すべてのenumキーを文字列として表す型を取得します。

ts
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
 
/**
* This is equivalent to:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
 
function printImportant(key: LogLevelStrings, message: string) {
const num = LogLevel[key];
if (num <= LogLevel.WARN) {
console.log("Log level key is:", key);
console.log("Log level value is:", num);
console.log("Log level message is:", message);
}
}
printImportant("ERROR", "This is a message");
Try

逆マッピング

メンバのプロパティ名を持つオブジェクトを作成することに加えて、数値enumメンバはenum値からenum名への*逆マッピング*も取得します。たとえば、この例では

ts
enum Enum {
A,
}
 
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
Try

TypeScriptはこれを次のJavaScriptにコンパイルします

ts
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
 
Try

この生成されたコードでは、enumは順方向(`name` -> `value`)と逆方向(`value` -> `name`)の両方のマッピングを格納するオブジェクトにコンパイルされます。他のenumメンバへの参照は、常にプロパティアクセスとして出力され、インライン化されることはありません。

文字列enumメンバは、逆マッピングがまったく生成されないことに注意してください。

`const` enum

ほとんどの場合、enumは完全に有効なソリューションです。ただし、要件がより厳格な場合があります。余分な生成コードのコストとenum値にアクセスする際の追加の間接参照を回避するために、`const` enumを使用することが可能です。`const` enumは、enumに`const`修飾子を使用して定義されます。

ts
const enum Enum {
A = 1,
B = A * 2,
}
Try

`const` enumは定数enum式のみを使用でき、通常のenumとは異なり、コンパイル中に完全に削除されます。`const` enumメンバは、使用場所でインライン化されます。これは、`const` enumが計算型メンバを持つことができないため可能です。

ts
const enum Direction {
Up,
Down,
Left,
Right,
}
 
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
Try

生成されたコードでは、次のようになります

ts
"use strict";
let directions = [
0 /* Direction.Up */,
1 /* Direction.Down */,
2 /* Direction.Left */,
3 /* Direction.Right */,
];
 
Try

`const` enumの落とし穴

enum値のインライン化は最初は簡単ですが、微妙な影響を及ぼします。これらの落とし穴は、*アンビエント* `const` enum(基本的には`.d.ts`ファイル内の`const` enum)とプロジェクト間での共有にのみ関係しますが、`.d.ts`ファイルを公開または使用している場合は、`tsc --declaration`が`.ts`ファイルを`.d.ts`ファイルに変換するため、これらの落とし穴が当てはまる可能性があります。

  1. `isolatedModules`のドキュメントに記載されている理由により、そのモードはアンビエント`const` enumと基本的に互換性がありません。これは、アンビエント`const` enumを公開する場合、ダウンストリームのコンシューマは`isolatedModules`とこれらのenum値を同時に使用できないことを意味します。
  2. コンパイル時に依存関係のバージョンAから値を簡単にインライン化し、ランタイムにバージョンBをインポートできます。バージョンAとBのenumは、非常に注意しないと異なる値を持つことができ、予期しないバグが発生する可能性があります。`if`ステートメントの誤った分岐を実行するなどです。これらのバグは、プロジェクトがビルドされるのとほぼ同時に、同じ依存関係バージョンで自動テストを実行するのが一般的であるため、これらのバグが完全に検出されないため、特に有害です。
  3. `importsNotUsedAsValues: "preserve"`は、値として使用される`const` enumのインポートを省略しませんが、アンビエント`const` enumはランタイム`.js`ファイルが存在することを保証しません。解決できないインポートは、ランタイムにエラーを引き起こします。インポートを明確に省略する通常の方法である型のみのインポートは、現在`const` enum値を許可していません

これらの落とし穴を回避するための2つのアプローチを以下に示します。

  1. `const` enumをまったく使用しない。リンターの助けを借りて、`const` enumを簡単に禁止できます。明らかに、これは`const` enumに関する問題を回避しますが、プロジェクトが独自のenumをインライン化することを妨げます。他のプロジェクトからのenumのインライン化とは異なり、プロジェクト独自のenumのインライン化は問題ではなく、パフォーマンスに影響します。

  2. `preserveConstEnums`の助けを借りて、`const`を削除することにより、アンビエント`const` enumを公開しない。これは、TypeScriptプロジェクト自体で内部的に採用されているアプローチです。`preserveConstEnums`は、`const` enumに対してプレーンenumと同じJavaScriptを出力します。その後、ビルドステップで`.d.ts`ファイルから`const`修飾子を安全に削除できます。

    このようにして、ダウンストリームのコンシューマはプロジェクトからenumをインライン化せず、上記の落とし穴を回避しますが、`const` enumを完全に禁止するとは異なり、プロジェクトは独自のenumをインライン化できます。

アンビエントenum

アンビエントenumは、既存のenum型の形状を記述するために使用されます。

ts
declare enum Enum {
A = 1,
B,
C = 2,
}
Try

アンビエントenumと非アンビエントenumの重要な違いの1つは、通常のenumでは、初期化子を持たないメンバは、前のenumメンバが定数と見なされる場合、定数と見なされることです。対照的に、初期化子を持たないアンビエント(および非`const`)enumメンバは、*常に*計算型と見なされます。

オブジェクト vs 列挙型

最新のTypeScriptでは、`as const` を使用したオブジェクトで十分な場合、列挙型は必要ないかもしれません。

ts
const enum EDirection {
Up,
Down,
Left,
Right,
}
 
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
 
EDirection.Up;
(enum member) EDirection.Up = 0
 
ODirection.Up;
(property) Up: 0
 
// Using the enum as a parameter
function walk(dir: EDirection) {}
 
// It requires an extra line to pull out the values
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
 
walk(EDirection.Left);
run(ODirection.Right);
Try

TypeScriptの `enum` よりもこの形式を支持する最大の理由は、コードベースをJavaScriptの現状と一致させることができ、将来JavaScriptに列挙型が追加された場合に追加の構文に移行できることです。

TypeScriptのドキュメントはオープンソースプロジェクトです。プルリクエストを送信して、これらのページの改善にご協力ください ❤ プルリクエストを送信する

このページの貢献者
OTOrta Therox (17)
FDG-SFrank de Groot - Schouten (1)
Ggreen961 (1)
TATex Andersen (1)
ETEric Telkkälä (1)
10+

最終更新日: 2024年3月21日