Как разные варианты перечисления работают в TypeScript?

116

В 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флаг компилятора - как он с ними взаимодействует?

Райан Кавано
источник
1
Я только что написал об этом статью , хотя она больше связана со сравнением перечислений const с неконстантными перечислениями
joelmdev 05

Ответы:

247

Вы должны знать четыре различных аспекта перечислений в TypeScript. Для начала несколько определений:

"объект поиска"

Если вы напишете это перечисление:

enum Foo { X, Y }

TypeScript выдаст следующий объект:

var Foo;
(function (Foo) {
    Foo[Foo["X"] = 0] = "X";
    Foo[Foo["Y"] = 1] = "Y";
})(Foo || (Foo = {}));

Я буду называть его поисковым объектом . Его цель двоякая: служить отображением строк в числа , например, при записи Foo.Xили Foo['X'], и служить отображением чисел в строки . Это обратное сопоставление полезно для целей отладки или ведения журнала - вы часто будете иметь значение 0или 1и хотите получить соответствующую строку "X"или "Y".

"объявить" или " окружающий "

В TypeScript вы можете «объявлять» вещи, о которых должен знать компилятор, но не генерировать код на самом деле. Это полезно, когда у вас есть библиотеки, такие как jQuery, которые определяют некоторый объект (например $), о котором вы хотите ввести информацию, но не нуждаетесь в каком-либо коде, созданном компилятором. Спецификация и другая документация ссылаются на сделанные таким образом объявления как на «окружающий» контекст; Важно отметить, что все объявления в .d.tsфайле являются «внешними» (либо требуют явного declareмодификатора, либо имеют его неявно, в зависимости от типа объявления).

"встраивание"

По соображениям производительности и размера кода часто предпочтительнее иметь ссылку на член перечисления, замененную его числовым эквивалентом при компиляции:

enum Foo { X = 4 }
var y = Foo.X; // emits "var y = 4";

В спецификации эта подстановка называется , я назову ее встраиванием, потому что она звучит круче. Иногда вам не нужно, чтобы члены перечисления были встроены, например, потому что значение перечисления может измениться в будущей версии API.


Enums, как они работают?

Давайте разберем это по каждому аспекту перечисления. К сожалению, каждый из этих четырех разделов будет ссылаться на термины из всех остальных, поэтому вам, вероятно, придется прочитать все это целиком более одного раза.

вычисленное vs невычисленное (константа)

Члены перечисления могут быть вычислены или нет. Спецификация вызовов , не вычисляемые члены постоянной , но я буду называть их не вычисленный , чтобы избежать путаницы с сопзЬ .

Вычисляются член перечисления один, значение которого не известно во время компиляции. Ссылки на вычисляемые члены, конечно, не могут быть встроены. И наоборот, не-вычисленный членом является перечислением раз, значение которого будет известно во время компиляции. Ссылки на невычисляемые элементы всегда встраиваются.

Какие члены перечисления вычисляются, а какие не вычисляются? Во-первых, все члены constперечисления постоянны (т. Е. Не вычисляются), как следует из названия. Для неконстантного перечисления это зависит от того, смотрите ли вы на внешнее (объявленное) перечисление или на внешнее перечисление.

Член declare enum(то есть окружающее перечисление) является константой тогда и только тогда, когда у него есть инициализатор. В противном случае он вычисляется. Обратите внимание, что в a declare enumразрешены только числовые инициализаторы. Пример:

declare enum Foo {
    X, // Computed
    Y = 2, // Non-computed
    Z, // Computed! Not 3! Careful!
    Q = 1 + 1 // Error
}

Наконец, элементы перечислений, не объявленных неконстантными, всегда считаются вычисленными. Однако их инициализирующие выражения сокращаются до констант, если они вычислимы во время компиляции. Это означает, что неконстантные члены перечисления никогда не встраиваются (это поведение изменилось в TypeScript 1.5, см. «Изменения в TypeScript» внизу)

константа против неконстантной

Const

Объявление перечисления может иметь constмодификатор. Если перечисление есть const, все ссылки на его члены встроены.

const enum Foo { A = 4 }
var x = Foo.A; // emitted as "var x = 4;", always

Перечисления const не создают объект поиска при компиляции. По этой причине ссылка Fooв приведенном выше коде является ошибкой, за исключением ссылки на член. FooВо время выполнения не будет никаких объектов.

неконстантный

Если объявление перечисления не имеет constмодификатора, ссылки на его элементы встроены, только если член не вычисляется. Неконстантное перечисление без объявления создаст объект поиска.

объявить (окружающий) vs не объявить

Важное предисловие состоит в том, что declareв TypeScript есть очень конкретное значение: этот объект существует где-то еще . Это для описания существующих объектов. Использование declareдля определения объектов, которые на самом деле не существуют, может иметь плохие последствия; мы рассмотрим их позже.

объявить

A declare enumне будет генерировать поисковый объект. Ссылки на его элементы встроены, если эти элементы вычисляются (см. Выше о вычисленных и невычисленных).

Важно отметить , что другие формы ссылки на declare enum будут разрешены, например , этот код не ошибка компиляции , но будет не в состоянии во время выполнения:

// 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!

A declare const enumне отличается от a const enum, за исключением случая --preserveConstEnums (см. Ниже).

не объявлять

Перечисление без объявления создает объект поиска, если это не так const. Встраивание описано выше.

--preserveConstEnums флаг

Этот флаг имеет только один эффект: перечисления констант без объявления будут генерировать объект поиска. Встраивание не затрагивается. Это полезно для отладки.


Общие ошибки

Самая распространенная ошибка - использовать declare enumвместо обычного 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]);
}

Помните золотое правило: никогда declareто, чего на самом деле не существует . Используйте, const enumесли вы всегда хотите встраивать, или enumесли вам нужен объект поиска.


Изменения в TypeScript

Между TypeScript 1.4 и 1.5 было изменение в поведении (см. Https://github.com/Microsoft/TypeScript/issues/2183 ), чтобы все члены не объявленных неконстантных перечислений считались вычисленными, даже если они явно инициализируются литералом. Это, так сказать, «нерасщепляет ребенка», делая встраиваемое поведение более предсказуемым и более четко отделяя концепцию const enumот обычного enum. До этого изменения невычисляемые члены неконстантных перечислений встраивались более агрессивно.

Райан Кавано
источник
6
Действительно отличный ответ. Он многое прояснил для меня, не только перечисления.
Clark
1
Хотел бы я проголосовать за вас больше одного раза ... не знал об этом критическом изменении. При правильном семантическом управлении версиями это можно было бы считать ударом по
сравнению
Очень полезное сравнение различных enumтипов, спасибо!
Мариус Шульц
@ Райан, это очень помогло, спасибо! Теперь нам просто нужен Web Essentials 2015 для создания правильных constдля объявленных типов перечислений.
Styfle
19
Этот ответ, кажется, очень подробно объясняет ситуацию в 1.4, а затем в самом конце он говорит: «Но 1.5 все изменил, и теперь все намного проще». Предполагая, что я все правильно понимаю, эта организация будет становиться все более и более неуместной по мере того, как этот ответ становится старше: я настоятельно рекомендую сначала изложить более простую текущую ситуацию и только после этого сказать: «но если вы используете 1.4 или более раннюю версию, все немного сложнее ».
KRyan
34

Здесь происходит несколько вещей. Давайте рассмотрим случай за случаем.

перечислить

enum Cheese { Brie, Cheddar }

Во-первых, простое старое перечисление. При компиляции в JavaScript будет создана таблица поиска.

Таблица поиска выглядит так:

var Cheese;
(function (Cheese) {
    Cheese[Cheese["Brie"] = 0] = "Brie";
    Cheese[Cheese["Cheddar"] = 1] = "Cheddar";
})(Cheese || (Cheese = {}));

Затем, когда у вас есть Cheese.BrieTypeScript, он излучает Cheese.Brieв JavaScript, который оценивается как 0. Cheese[0]излучает Cheese[0]и фактически оценивает "Brie".

константное перечисление

const enum Bread { Rye, Wheat }

На самом деле никакого кода для этого не создается! Его значения встроены. Следующее излучает само значение 0 в JavaScript:

Bread.Rye
Bread['Rye']

const enums 'встраивание может быть полезно по соображениям производительности.

А как насчет Bread[0]? Это приведет к ошибке во время выполнения, и ваш компилятор должен ее уловить. Здесь нет таблицы поиска и компилятор не встроен.

Обратите внимание, что в приведенном выше случае флаг --preserveConstEnums заставит Bread сгенерировать таблицу поиска. Однако его значения по-прежнему будут встроены.

объявить перечисление

Как и в случае с другими вариантами использования declare, не declareгенерирует код и ожидает, что вы определили фактический код где-то еще. Это не создает таблицу поиска:

declare enum Wine { Red, Wine }

Wine.Redизлучается Wine.Redв JavaScript, но не будет никакой справочной таблицы Wine для ссылки, поэтому это ошибка, если вы не определили ее где-либо еще.

объявить константное перечисление

Это не создает таблицу поиска:

declare const enum Fruit { Apple, Pear }

Но он встроен! Fruit.Appleвыдает 0. Но снова выдает Fruit[0]ошибку во время выполнения, потому что он не встроен и нет таблицы поиска.

Я написал это на этой игровой площадке. Я рекомендую поиграть там, чтобы понять, какой TypeScript какой JavaScript испускает.

Kat
источник
1
Я рекомендую обновить этот ответ: Начиная с Typescript 3.3.3, Bread[0]выдает ошибку компилятора: «К члену константного перечисления можно получить доступ только с помощью строкового литерала».
chharvey
1
Хм ... это отличается от того, что написано в ответе? «Но как насчет Bread [0]? Это приведет к ошибке во время выполнения, и ваш компилятор должен ее уловить. Нет таблицы поиска и компилятор здесь не встроен».
Кэт