列挙型は、TypeScriptが持つ数少ない機能の1つであり、JavaScriptの型レベルの拡張ではありません。
列挙型を使用すると、開発者は名前付き定数のセットを定義できます。 列挙型を使用すると、意図の文書化や、個別のケースのセットの作成が容易になります。 TypeScriptは、数値ベースと文字列ベースの両方の列挙型を提供します。
数値列挙型
最初に数値列挙型から始めます。これは、他の言語から来ている場合、おそらくより馴染み深いでしょう。 列挙型は、enum
キーワードを使用して定義できます。
tsTry
enumDirection {Up = 1,Down ,Left ,Right ,}
上記では、Up
が1
で初期化されている数値列挙型があります。 後続のすべてのメンバーは、その時点から自動的にインクリメントされます。 つまり、Direction.Up
の値は1
、Down
は2
、Left
は3
、Right
は4
です。
必要に応じて、初期化子を完全に省略できます
tsTry
enumDirection {Up ,Down ,Left ,Right ,}
ここで、Up
の値は0
、Down
は1
などになります。 この自動インクリメントの動作は、メンバー値自体には関心がなく、各値が同じ列挙型の他の値と異なることだけに関心がある場合に役立ちます。
列挙型の使用は簡単です。列挙型自体のプロパティとして任意のメンバーにアクセスし、列挙型の名前を使用して型を宣言するだけです
tsTry
enumUserResponse {No = 0,Yes = 1,}functionrespond (recipient : string,message :UserResponse ): void {// ...}respond ("Princess Caroline",UserResponse .Yes );
数値列挙型は、計算されたメンバーと定数メンバー(下記参照)に混在させることができます。 簡単に言うと、初期化子がない列挙型は、最初にするか、数値定数または他の定数列挙型メンバーで初期化された数値列挙型の後に配置する必要があります。 つまり、以下は許可されていません
tsTry
enumE {A =getSomeValue (),Enum member must have initializer.1061Enum member must have initializer., B }
文字列列挙型
文字列列挙型も同様の概念ですが、以下に示すように、いくつかの微妙なランタイムの違いがあります。 文字列列挙型では、各メンバーは文字列リテラル、または別の文字列列挙型メンバーで定数初期化する必要があります。
tsTry
enumDirection {Up = "UP",Down = "DOWN",Left = "LEFT",Right = "RIGHT",}
文字列列挙型には自動インクリメントの動作はありませんが、文字列列挙型には「シリアライズ」しやすいという利点があります。 つまり、デバッグ中に数値列挙型のランタイム値を読み取る必要がある場合、値は多くの場合不透明です。つまり、それだけでは有用な意味を伝えません(ただし、逆マッピングはしばしば役立ちます)。 文字列列挙型を使用すると、列挙型メンバー自体の名前とは無関係に、コードの実行時に意味のある読み取り可能な値を与えることができます。
異種混合enum
技術的には、enumは文字列と数値のメンバーを混在させることができますが、なぜそうしたいのか明確な理由はありません。
tsTry
enumBooleanLikeHeterogeneousEnum {No = 0,Yes = "YES",}
JavaScriptのランタイム動作を巧妙に利用しようとしているのでない限り、これを行うことはお勧めしません。
計算型メンバーと定数メンバー
各enumメンバーには、定数または計算型のいずれかである値が関連付けられています。enumメンバーは、以下の場合に定数とみなされます。
-
enumの最初のメンバーであり、初期化子がなく、値
0
が割り当てられている場合。ts
Try// E.X is constant:enumE {X ,} -
初期化子がなく、前のenumメンバーが数値定数であった場合。この場合、現在のenumメンバーの値は、前のenumメンバーの値に1を加えた値になります。
ts
Try// All enum members in 'E1' and 'E2' are constant.enumE1 {X ,Y ,Z ,}enumE2 {A = 1,B ,C ,} -
enumメンバーが定数enum式で初期化されている場合。定数enum式は、コンパイル時に完全に評価できるTypeScript式のサブセットです。式は、以下の場合に定数enum式となります。
- リテラルenum式(基本的には文字列リテラルまたは数値リテラル)
- 以前に定義された定数enumメンバーへの参照(別のenumから派生する場合もある)
- 括弧で囲まれた定数enum式
- 定数enum式に適用される単項演算子
+
、-
、~
のいずれか - オペランドとして定数enum式を持つ二項演算子
+
、-
、*
、/
、%
、<<
、>>
、>>>
、&
、|
、^
定数enum式が
NaN
またはInfinity
に評価されるのは、コンパイル時エラーです。
その他すべての場合、enumメンバーは計算型とみなされます。
tsTry
enumFileAccess {// constant membersNone ,Read = 1 << 1,Write = 1 << 2,ReadWrite =Read |Write ,// computed memberG = "123".length ,}
ユニオンenumとenumメンバー型
計算されない定数enumメンバーの特別なサブセットがあります。リテラルenumメンバーです。リテラルenumメンバーは、初期化されていない値、または以下に初期化された値を持つ定数enumメンバーです。
- 任意の文字列リテラル(例:
"foo"
、"bar"
、"baz"
) - 任意の数値リテラル(例:
1
、100
) - 任意の数値リテラルに適用される単項マイナス(例:
-1
、-100
)
enumのすべてのメンバーがリテラルenum値を持つ場合、いくつかの特別なセマンティクスが作用します。
1つ目は、enumメンバーも型になることです!たとえば、特定のメンバーがenumメンバーの値のみを持つことができると指定できます。
tsTry
enumShapeKind {Circle ,Square ,}interfaceCircle {kind :ShapeKind .Circle ;radius : number;}interfaceSquare {kind :ShapeKind .Square ;sideLength : number;}letc :Circle = {Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.2322Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.: kind ShapeKind .Square ,radius : 100,};
もう1つの変更点は、enum型自体が事実上、各enumメンバーの*ユニオン*になることです。ユニオンenumを使用すると、型システムはenum自体に存在する値の正確なセットを知っているという事実を活用できます。そのため、TypeScriptは値を誤って比較している可能性のあるバグをキャッチできます。例えば
tsTry
enumE {Foo ,Bar ,}functionf (x :E ) {if (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.x !==E .Foo ||x !==E .Bar ) {//}}
この例では、最初にx
がE.Foo
*ではない*かどうかを確認しました。そのチェックが成功した場合、||
はショートサーキットし、「if」の本体が実行されます。ただし、チェックが成功しなかった場合、x
は*必ず*E.Foo
になるので、E.Bar
と*等しくない*かどうかを確認するのは意味がありません。
ランタイム時のenum
enumは、ランタイムに存在する実際のオブジェクトです。たとえば、次のenum
tsTry
enumE {X ,Y ,Z ,}
は、実際に関数に渡すことができます。
tsTry
enumE {X ,Y ,Z ,}functionf (obj : {X : number }) {returnobj .X ;}// Works, since 'E' has a property named 'X' which is a number.f (E );
コンパイル時のenum
enumはランタイムに存在する実際のオブジェクトですが、`keyof`キーワードは一般的なオブジェクトの場合とは異なる動作をします。代わりに、`keyof typeof`を使用して、すべてのenumキーを文字列として表す型を取得します。
tsTry
enumLogLevel {ERROR ,WARN ,INFO ,DEBUG ,}/*** This is equivalent to:* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';*/typeLogLevelStrings = keyof typeofLogLevel ;functionprintImportant (key :LogLevelStrings ,message : string) {constnum =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");
逆マッピング
メンバのプロパティ名を持つオブジェクトを作成することに加えて、数値enumメンバはenum値からenum名への*逆マッピング*も取得します。たとえば、この例では
tsTry
enumEnum {A ,}leta =Enum .A ;letnameOfA =Enum [a ]; // "A"
TypeScriptはこれを次のJavaScriptにコンパイルします
tsTry
"use strict";var Enum;(function (Enum) {Enum[Enum["A"] = 0] = "A";})(Enum || (Enum = {}));let a = Enum.A;let nameOfA = Enum[a]; // "A"
この生成されたコードでは、enumは順方向(`name` -> `value`)と逆方向(`value` -> `name`)の両方のマッピングを格納するオブジェクトにコンパイルされます。他のenumメンバへの参照は、常にプロパティアクセスとして出力され、インライン化されることはありません。
文字列enumメンバは、逆マッピングがまったく生成されないことに注意してください。
`const` enum
ほとんどの場合、enumは完全に有効なソリューションです。ただし、要件がより厳格な場合があります。余分な生成コードのコストとenum値にアクセスする際の追加の間接参照を回避するために、`const` enumを使用することが可能です。`const` enumは、enumに`const`修飾子を使用して定義されます。
tsTry
const enumEnum {A = 1,B =A * 2,}
`const` enumは定数enum式のみを使用でき、通常のenumとは異なり、コンパイル中に完全に削除されます。`const` enumメンバは、使用場所でインライン化されます。これは、`const` enumが計算型メンバを持つことができないため可能です。
tsTry
const enumDirection {Up ,Down ,Left ,Right ,}letdirections = [Direction .Up ,Direction .Down ,Direction .Left ,Direction .Right ,];
生成されたコードでは、次のようになります
tsTry
"use strict";let directions = [0 /* Direction.Up */,1 /* Direction.Down */,2 /* Direction.Left */,3 /* Direction.Right */,];
`const` enumの落とし穴
enum値のインライン化は最初は簡単ですが、微妙な影響を及ぼします。これらの落とし穴は、*アンビエント* `const` enum(基本的には`.d.ts`ファイル内の`const` enum)とプロジェクト間での共有にのみ関係しますが、`.d.ts`ファイルを公開または使用している場合は、`tsc --declaration`が`.ts`ファイルを`.d.ts`ファイルに変換するため、これらの落とし穴が当てはまる可能性があります。
- `isolatedModules`のドキュメントに記載されている理由により、そのモードはアンビエント`const` enumと基本的に互換性がありません。これは、アンビエント`const` enumを公開する場合、ダウンストリームのコンシューマは`isolatedModules`とこれらのenum値を同時に使用できないことを意味します。
- コンパイル時に依存関係のバージョンAから値を簡単にインライン化し、ランタイムにバージョンBをインポートできます。バージョンAとBのenumは、非常に注意しないと異なる値を持つことができ、予期しないバグが発生する可能性があります。`if`ステートメントの誤った分岐を実行するなどです。これらのバグは、プロジェクトがビルドされるのとほぼ同時に、同じ依存関係バージョンで自動テストを実行するのが一般的であるため、これらのバグが完全に検出されないため、特に有害です。
- `importsNotUsedAsValues: "preserve"`は、値として使用される`const` enumのインポートを省略しませんが、アンビエント`const` enumはランタイム`.js`ファイルが存在することを保証しません。解決できないインポートは、ランタイムにエラーを引き起こします。インポートを明確に省略する通常の方法である型のみのインポートは、現在`const` enum値を許可していません。
これらの落とし穴を回避するための2つのアプローチを以下に示します。
-
`const` enumをまったく使用しない。リンターの助けを借りて、`const` enumを簡単に禁止できます。明らかに、これは`const` enumに関する問題を回避しますが、プロジェクトが独自のenumをインライン化することを妨げます。他のプロジェクトからのenumのインライン化とは異なり、プロジェクト独自のenumのインライン化は問題ではなく、パフォーマンスに影響します。
-
`preserveConstEnums`の助けを借りて、`const`を削除することにより、アンビエント`const` enumを公開しない。これは、TypeScriptプロジェクト自体で内部的に採用されているアプローチです。`preserveConstEnums`は、`const` enumに対してプレーンenumと同じJavaScriptを出力します。その後、ビルドステップで`.d.ts`ファイルから`const`修飾子を安全に削除できます。
このようにして、ダウンストリームのコンシューマはプロジェクトからenumをインライン化せず、上記の落とし穴を回避しますが、`const` enumを完全に禁止するとは異なり、プロジェクトは独自のenumをインライン化できます。
アンビエントenum
アンビエントenumは、既存のenum型の形状を記述するために使用されます。
tsTry
declare enumEnum {A = 1,B ,C = 2,}
アンビエントenumと非アンビエントenumの重要な違いの1つは、通常のenumでは、初期化子を持たないメンバは、前のenumメンバが定数と見なされる場合、定数と見なされることです。対照的に、初期化子を持たないアンビエント(および非`const`)enumメンバは、*常に*計算型と見なされます。
オブジェクト vs 列挙型
最新のTypeScriptでは、`as const` を使用したオブジェクトで十分な場合、列挙型は必要ないかもしれません。
tsTry
const enumEDirection {Up ,Down ,Left ,Right ,}constODirection = {Up : 0,Down : 1,Left : 2,Right : 3,} asconst ;EDirection .Up ;ODirection .Up ;// Using the enum as a parameterfunctionwalk (dir :EDirection ) {}// It requires an extra line to pull out the valuestypeDirection = typeofODirection [keyof typeofODirection ];functionrun (dir :Direction ) {}walk (EDirection .Left );run (ODirection .Right );
TypeScriptの `enum` よりもこの形式を支持する最大の理由は、コードベースをJavaScriptの現状と一致させることができ、将来JavaScriptに列挙型が追加された場合に追加の構文に移行できることです。