Como as diferentes variantes de enum funcionam no TypeScript?

116

O TypeScript tem várias maneiras diferentes de definir um enum:

enum Alpha { X, Y, Z }
const enum Beta { X, Y, Z }
declare enum Gamma { X, Y, Z }
declare const enum Delta { X, Y, Z }

Se tento usar um valor Gammaem tempo de execução, obtenho um erro porque Gammanão está definido, mas não é o caso de Deltaou Alpha? O que significa constou declaresignifica nas declarações aqui?

Há também um preserveConstEnumssinalizador do compilador - como isso interage com eles?

Ryan Cavanaugh
fonte
1
Acabei de escrever um artigo sobre isso , embora tenha mais a ver com a comparação de const com non const enums
joelmdev

Respostas:

246

Existem quatro aspectos diferentes dos enums no TypeScript que você precisa conhecer. Primeiro, algumas definições:

"objeto de pesquisa"

Se você escrever este enum:

enum Foo { X, Y }

O TypeScript emitirá o seguinte objeto:

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

Vou me referir a isso como o objeto de pesquisa . Seu propósito é duplo: servir como um mapeamento de strings para números , por exemplo, ao escrever Foo.Xou Foo['X'], e servir como um mapeamento de números para strings . Esse mapeamento reverso é útil para fins de depuração ou registro - você geralmente terá o valor 0ou 1e deseja obter a string correspondente "X"ou "Y".

"declarar" ou " ambiente "

No TypeScript, você pode "declarar" coisas que o compilador deve saber, mas não para as quais realmente emitir código. Isso é útil quando você tem bibliotecas como jQuery que definem algum objeto (por exemplo $) sobre o qual deseja digitar informações, mas não precisa de nenhum código criado pelo compilador. A especificação e outras documentações referem-se a declarações feitas dessa maneira como sendo em um contexto "ambiente"; é importante observar que todas as declarações em um .d.tsarquivo são "ambientais" (exigindo um declaremodificador explícito ou tendo-o implicitamente, dependendo do tipo de declaração).

"inlining"

Por motivos de desempenho e tamanho do código, geralmente é preferível ter uma referência a um membro enum substituída por seu equivalente numérico quando compilado:

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

A especificação chama isso de substituição , vou chamá-lo inlining porque soa mais legal. Às vezes, você não vai querer que os membros do enum sejam sequenciais, por exemplo, porque o valor do enum pode mudar em uma versão futura da API.


Enums, como funcionam?

Vamos dividir isso por cada aspecto de um enum. Infelizmente, cada uma dessas quatro seções fará referência a termos de todas as outras, então você provavelmente precisará ler tudo isso mais de uma vez.

calculado vs não calculado (constante)

Os membros de Enum podem ser calculados ou não. A especificação chama constantes de membros não computados , mas vou chamá-los de não computados para evitar confusão com const .

Um membro enum calculado é aquele cujo valor não é conhecido em tempo de compilação. As referências a membros computados não podem ser sequenciais, é claro. Por outro lado, um membro enum não calculado é uma vez cujo valor é conhecido em tempo de compilação. As referências a membros não computados são sempre sequenciais.

Quais membros enum são computados e quais não são? Primeiro, todos os membros de um constenum são constantes (ou seja, não calculados), como o nome indica. Para um enum não constante, depende se você está olhando um enum ambiente (declarar) ou um enum não ambiente.

Um membro de a declare enum(isto é, enum ambiente) é constante se e somente se tiver um inicializador. Caso contrário, é calculado. Observe que em a declare enum, apenas inicializadores numéricos são permitidos. Exemplo:

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

Finalmente, os membros de enums não declarados não constantes são sempre considerados computados. No entanto, suas expressões de inicialização são reduzidas a constantes se forem computáveis ​​em tempo de compilação. Isso significa que os membros enum não constantes nunca são embutidos (esse comportamento mudou no TypeScript 1.5, consulte "Alterações no TypeScript" na parte inferior)

const vs não const

const

Uma declaração enum pode ter o constmodificador. Se um enum for const, todas as referências a seus membros serão incorporadas.

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

enums const não produzem um objeto de pesquisa quando compilados. Por esse motivo, é um erro fazer referência Foono código acima, exceto como parte de uma referência de membro. Nenhum Fooobjeto estará presente no tempo de execução.

não const

Se uma declaração enum não tiver o constmodificador, as referências a seus membros serão sequenciadas apenas se o membro não for computado. Um enum não const e não declarado produzirá um objeto lookup.

declarar (ambiente) vs não declarar

Um prefácio importante é que declareno TypeScript tem um significado muito específico: este objeto existe em outro lugar . É para descrever objetos existentes . Usar declarepara definir objetos que não existem realmente pode ter consequências ruins; vamos explorar isso mais tarde.

declarar

A declare enumnão emitirá um objeto de pesquisa. As referências aos seus membros são sequenciais se esses membros forem computados (veja acima em computado vs não computado).

É importante notar que as outras formas de referência a um declare enum são permitidos, por exemplo, este código é não um erro de compilação, mas irá falhar em tempo de execução:

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

Este erro se enquadra na categoria de "Não minta para o compilador". Se você não tiver um objeto nomeado Fooem tempo de execução, não escreva declare enum Foo!

A declare const enumnão é diferente de a const enum, exceto no caso de --preserveConstEnums (veja abaixo).

não declarar

Um enum não declarado produz um objeto de pesquisa se não for const. Inlining é descrito acima.

--preserveConstEnums sinalizador

Este sinalizador tem exatamente um efeito: enums const não declarados emitirá um objeto de pesquisa. Inlining não é afetado. Isso é útil para depuração.


Erros comuns

O erro mais comum é usar um declare enumquando um normal enumou const enumseria mais apropriado. Uma forma comum é esta:

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]);
}

Lembre-se da regra de ouro: nunca declarecoisas que não existam de verdade . Use const enumse quiser sempre inlining ou enumse quiser o objeto de pesquisa.


Mudanças no TypeScript

Entre TypeScript 1.4 e 1.5, houve uma mudança no comportamento (consulte https://github.com/Microsoft/TypeScript/issues/2183 ) para fazer com que todos os membros de enums não declarados não constantes fossem tratados como calculados, mesmo que eles são inicializados explicitamente com um literal. Isso "desfaz o bebê", por assim dizer, tornando o comportamento inlining mais previsível e separando de forma mais clara o conceito de const enumregular enum. Antes dessa mudança, os membros não computados de enums não constantes eram embutidos de forma mais agressiva.

Ryan Cavanaugh
fonte
6
Uma resposta realmente incrível. Isso esclareceu muitas coisas para mim, não apenas enums.
Clark
1
Eu gostaria de poder votar em você mais de uma vez ... não sabia sobre essa alteração significativa. Na versão semântica adequada, isso pode ser considerado um choque para a versão principal: - /
mfeineis
Uma comparação muito útil dos vários enumtipos, obrigado!
Marius Schulz
@Ryan isso é muito útil, obrigado! Agora, precisamos apenas do Web Essentials 2015 para produzir o apropriado constpara os tipos enum declarados.
elegante
19
Esta resposta parece entrar em detalhes explicando uma situação em 1.4 e, no final, diz “mas 1.5 mudou tudo e agora é muito mais simples”. Supondo que entendi as coisas corretamente, esta organização ficará cada vez mais inadequada à medida que esta resposta envelhece: Eu recomendo enfaticamente colocar a situação atual mais simples primeiro , e somente depois disso dizendo “mas se você estiver usando 1.4 ou anterior, coisas são um pouco mais complicados. ”
KRyan
33

Existem algumas coisas acontecendo aqui. Vamos caso a caso.

enum

enum Cheese { Brie, Cheddar }

Primeiro, um enum simples e antigo. Quando compilado para JavaScript, ele emitirá uma tabela de pesquisa.

A tabela de pesquisa é semelhante a esta:

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

Então, quando você tem o Cheese.BrieTypeScript, ele emite Cheese.Brieem JavaScript que é avaliado como 0. Cheese[0]emite Cheese[0]e realmente avalia como "Brie".

const enum

const enum Bread { Rye, Wheat }

Nenhum código é realmente emitido para isso! Seus valores estão embutidos. O seguinte emite o próprio valor 0 em JavaScript:

Bread.Rye
Bread['Rye']

const enums 'inlining pode ser útil por motivos de desempenho.

Mas e quanto Bread[0]? Isso ocorrerá em tempo de execução e seu compilador deve detectá-lo. Não há tabela de pesquisa e o compilador não embutido aqui.

Observe que, no caso acima, o sinalizador --preserveConstEnums fará com que o Bread emita uma tabela de pesquisa. Seus valores ainda estarão embutidos.

declarar enum

Tal como acontece com outros usos de declare, declarenão emite código e espera que você tenha definido o código real em outro lugar. Isso não emite uma tabela de pesquisa:

declare enum Wine { Red, Wine }

Wine.Redemite Wine.Redem JavaScript, mas não haverá nenhuma tabela de pesquisa Wine para referência, então é um erro, a menos que você tenha definido em outro lugar.

declarar const enum

Isso não emite uma tabela de pesquisa:

declare const enum Fruit { Apple, Pear }

Mas faz inline! Fruit.Appleemite 0. Mas, novamente, Fruit[0]apresentará um erro no tempo de execução porque não é embutido e não há tabela de pesquisa.

Eu escrevi isso neste playground. Recomendo jogar lá para entender qual TypeScript emite qual JavaScript.

Kat
fonte
1
Eu recomendo atualizar esta resposta: A partir do Typescript 3.3.3, Bread[0]lança um erro do compilador: “Um membro const enum só pode ser acessado usando um literal de string.”
chharvey
1
Hm ... isso é diferente do que diz a resposta? "Mas e o Bread [0]? Isso apresentará um erro no tempo de execução e seu compilador deve detectá-lo. Não há tabela de pesquisa e o compilador não embutido aqui."
Kat