TypeScriptには、列挙型を定義するさまざまな方法があります。
enum Alpha { X, Y, Z }
const enum Beta { X, Y, Z }
declare enum Gamma { X, Y, Z }
declare const enum Delta { X, Y, Z }
実行時にGamma
の値を使用しようとすると、Gamma
が定義されていないためエラーが発生しますが、Delta
またはAlpha
には該当しません。ここでの宣言でconst
またはdeclare
はどういう意味ですか?
preserveConstEnums
コンパイラフラグもあります。これはこれらとどのように相互作用しますか?
TypeScriptで列挙する必要がある4つの異なる側面があります。まず、いくつかの定義:
この列挙型を記述する場合:
enum Foo { X, Y }
TypeScriptは次のオブジェクトを発行します。
var Foo;
(function (Foo) {
Foo[Foo["X"] = 0] = "X";
Foo[Foo["Y"] = 1] = "Y";
})(Foo || (Foo = {}));
これをlookup objectと呼びます。その目的は2つあります。stringsからnumbersへのマッピングとして機能すること。 Foo.X
またはFoo['X']
を記述し、numbersからstringsへのマッピングとして機能する場合。この逆マッピングは、デバッグやログ記録の目的に役立ちます。多くの場合、値は0
または1
になり、対応する文字列"X"
または"Y"
を取得します。
TypeScriptでは、コンパイラが知っておくべきことを「宣言」できますが、実際にコードを発行することはできません。これは、型情報が必要なオブジェクト(例:$
)を定義するjQueryのようなライブラリがあるが、コンパイラによって作成されたコードは必要ない場合に便利です。仕様およびその他のドキュメントでは、このようにして行われた宣言が「アンビエント」コンテキストにあると言及しています。 .d.ts
ファイル内のすべての宣言は「アンビエント」であることに注意することが重要です(明示的なdeclare
修飾子を必要とするか、宣言タイプに応じて暗黙的にそれを保持します)。
パフォーマンスとコードサイズの理由から、コンパイル時に列挙メンバーへの参照を同等の数値に置き換えることが望ましい場合がよくあります。
enum Foo { X = 4 }
var y = Foo.X; // emits "var y = 4";
仕様ではこれをsubstitutionと呼びますが、私はinliningと呼びます。たとえば、enum値はAPIの将来のバージョンで変更される可能性があるため、enumメンバーをインライン化したい場合はnotにします。
これを列挙型の各側面で分類しましょう。残念ながら、これら4つのセクションはそれぞれ、他のすべてのセクションから用語を参照するため、おそらくこの記事全体を複数回読む必要があります。
列挙型のメンバーは、computedのいずれかです。仕様は非計算メンバーconstantを呼び出しますが、constとの混乱を避けるためにnon-computedと呼び出します。
computedenumメンバは、コンパイル時に値がわからないメンバです。もちろん、計算されたメンバーへの参照はインライン化できません。逆に、non-computed列挙型メンバーは、コンパイル時に値isがわかっている一度だけです。非計算メンバーへの参照は常にインライン化されます。
どの列挙メンバーが計算され、どれが計算されませんか?最初に、const
列挙型のすべてのメンバーは、名前が示すように定数(つまり、計算されていない)です。非const列挙型の場合、ambient(宣言)列挙型か非周囲型列挙型のどちらを表示しているかによって異なります。
declare enum
(アンビエント列挙型)のメンバーは、定数ifであり、ifにのみ初期化子があります。それ以外の場合は、計算されます。 declare enum
では、数値初期化子のみが許可されることに注意してください。例:
declare enum Foo {
X, // Computed
Y = 2, // Non-computed
Z, // Computed! Not 3! Careful!
Q = 1 + 1 // Error
}
最後に、非宣言非定数列挙型のメンバーは常に計算されていると見なされます。ただし、コンパイル時に計算可能な場合、初期化式は定数になります。これは、非const列挙型メンバーがインライン化されないことを意味します(この動作はTypeScript 1.5で変更されました。下部の「TypeScriptの変更」を参照してください)
Enum宣言には、const
修飾子を含めることができます。列挙型がconst
の場合、allのメンバーへの参照がインライン化されます。
const enum Foo { A = 4 }
var x = Foo.A; // emitted as "var x = 4;", always
const列挙型は、コンパイル時にルックアップオブジェクトを生成しません。このため、上記のコードでFoo
を参照すると、メンバー参照の一部として使用する場合を除き、エラーになります。実行時にFoo
オブジェクトは存在しません。
Enum宣言にconst
修飾子がない場合、そのメンバーへの参照は、メンバーが計算されていない場合にのみインライン化されます。非定数、非宣言の列挙型は、ルックアップオブジェクトを生成します。
重要な序文は、TypeScriptのdeclare
には非常に具体的な意味があることです。このオブジェクトは、別の場所に存在します。existingオブジェクトを記述するためのものです。 declare
を使用して実際に存在しないオブジェクトを定義すると、悪い結果を招く可能性があります。これらについては後で説明します。
declare enum
はルックアップオブジェクトを発行しません。それらのメンバーが計算される場合、そのメンバーへの参照はインライン化されます(上記の計算済みと非計算済みを参照)。
declare enum
areへの他の参照形式が許可されていることに注意することが重要です。このコードはnotコンパイルエラーですが、willは実行時に失敗します。
// Note: Assume no other file has actually created a Foo var at runtime
declare enum Foo { Bar }
var s = 'Bar';
var b = Foo[s]; // Fails
このエラーは、「コンパイラに嘘をつかないでください」というカテゴリに分類されます。実行時にFoo
という名前のオブジェクトがない場合は、declare enum Foo
を記述しないでください!
declare const enum
はconst enum
と違いはありませんが、-preserveConstEnumsの場合を除きます(以下を参照)。
宣言されていない列挙型は、const
でない場合にルックアップオブジェクトを生成します。インライン化については上記で説明しています。
このフラグには、1つの効果があります。非宣言const列挙型は、ルックアップオブジェクトを出力します。インライン化は影響を受けません。これはデバッグに役立ちます。
最も一般的な間違いは、通常のenum
またはdeclare enum
がより適切な場合にconst enum
を使用することです。一般的な形式は次のとおりです。
module MyModule {
// Claiming this enum exists with 'declare', but it doesn't...
export declare enum Lies {
Foo = 0,
Bar = 1
}
var x = Lies.Foo; // Depend on inlining
}
module SomeOtherCode {
// x ends up as 'undefined' at runtime
import x = MyModule.Lies;
// Try to use lookup object, which ought to exist
// runtime error, canot read property 0 of undefined
console.log(x[x.Foo]);
}
黄金のルールを覚えておいてください:Never declare
実際には存在しないもの。常にインライン化する場合はconst enum
を使用し、ルックアップオブジェクトが必要な場合はenum
を使用します。
TypeScript 1.4と1.5の間で、動作が変更されました( https://github.com/Microsoft/TypeScript/issues/218 を参照)。非宣言の非const列挙型のすべてのメンバーを作成します。リテラルで明示的に初期化されている場合でも、計算済みとして扱われます。これは、いわば「分断されていない赤ちゃん」であり、インライン動作をより予測可能にし、const enum
の概念を通常のenum
からより明確に分離します。この変更の前は、非const列挙型の非計算メンバーがより積極的にインライン化されていました。
ここでいくつかのことが行われています。ケースバイケースで行きましょう。
_enum Cheese { Brie, Cheddar }
_
まず、単純な古い列挙型。 JavaScriptにコンパイルすると、ルックアップテーブルが生成されます。
ルックアップテーブルは次のようになります。
_var Cheese;
(function (Cheese) {
Cheese[Cheese["Brie"] = 0] = "Brie";
Cheese[Cheese["Cheddar"] = 1] = "Cheddar";
})(Cheese || (Cheese = {}));
_
次に、TypeScriptに_Cheese.Brie
_があると、0に評価されるJavaScriptで_Cheese.Brie
_を出力します。_Cheese[0]
_は_Cheese[0]
_を出力し、実際に_"Brie"
_に評価されます。
_const enum Bread { Rye, Wheat }
_
このために実際にコードは出力されません!その値はインライン化されます。以下は、JavaScriptで値0自体を出力します。
_Bread.Rye
Bread['Rye']
_
_const enum
_ sのインライン化は、パフォーマンス上の理由で役立つ場合があります。
しかし、_Bread[0]
_はどうですか?これは実行時にエラーになり、コンパイラがそれをキャッチします。ルックアップテーブルはなく、コンパイラはここにインライン化されません。
上記の場合、-preserveConstEnumsフラグにより、Breadがルックアップテーブルを発行することに注意してください。ただし、その値はインライン化されます。
declare
の他の用途と同様に、declare
はコードを出力せず、実際のコードを他の場所で定義したことを期待します。これはルックアップテーブルを出力しません。
_declare enum Wine { Red, Wine }
_
_Wine.Red
_はJavaScriptで_Wine.Red
_を出力しますが、参照するWineルックアップテーブルがないため、他で定義しない限りエラーになります。
これはルックアップテーブルを出力しません。
_declare const enum Fruit { Apple, Pear }
_
しかし、それはインラインです! _Fruit.Apple
_は0を出力します。ただし、_Fruit[0]
_はインライン化されておらず、ルックアップテーブルがないため、実行時にエラーになります。
これは this playgroundで書きました。どのTypeScriptがどのJavaScriptを出力するかを理解するために、そこでプレイすることをお勧めします。