¿Cómo funcionan las diferentes variantes de enumeración en TypeScript?

116

TypeScript tiene varias formas diferentes de definir una enumeración:

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

Si trato de usar un valor de Gammaen tiempo de ejecución, obtengo un error porque Gammano está definido, pero ese no es el caso de Deltao Alpha? ¿Qué significa consto declaresignifican las declaraciones aquí?

También hay una preserveConstEnumsmarca de compilador: ¿cómo interactúa esto con estos?

Ryan Cavanaugh
fuente
1
Acabo de escribir un artículo sobre esto , aunque tiene más que ver con comparar enumeraciones constantes con no constantes
joelmdev

Respuestas:

247

Hay cuatro aspectos diferentes de las enumeraciones en TypeScript que debe conocer. Primero, algunas definiciones:

"objeto de búsqueda"

Si escribe esta enumeración:

enum Foo { X, Y }

TypeScript emitirá el siguiente objeto:

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

Me referiré a esto como el objeto de búsqueda . Su propósito es doble: servir como un mapeo de cadenas a números , por ejemplo, al escribir Foo.Xo Foo['X'], y servir como un mapeo de números a cadenas . Ese mapeo inverso es útil para fines de depuración o registro; a menudo tendrá el valor 0o 1y querrá obtener la cadena correspondiente "X"o "Y".

"declarar" o " ambiente "

En TypeScript, puede "declarar" cosas que el compilador debería conocer, pero no emitir código. Esto es útil cuando tiene bibliotecas como jQuery que definen algún objeto (por ejemplo $) sobre el que desea escribir información, pero no necesita ningún código creado por el compilador. La especificación y otra documentación se refiere a declaraciones hechas de esta manera como en un contexto "ambiental"; Es importante tener en cuenta que todas las declaraciones de un .d.tsarchivo son "ambientales" (ya sea que requieran un declaremodificador explícito o lo tengan implícito, según el tipo de declaración).

"alineado"

Por razones de rendimiento y tamaño del código, a menudo es preferible tener una referencia a un miembro enum reemplazado por su equivalente numérico cuando se compila:

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

La especificación llama a esta sustitución , yo la llamaré inlining porque suena mejor. A veces se quiere no querrá que los miembros de enumeración estén incluidos, por ejemplo, porque el valor de enumeración podría cambiar en una versión futura de la API.


Enums, ¿cómo funcionan?

Analicemos esto por cada aspecto de una enumeración. Desafortunadamente, cada una de estas cuatro secciones hará referencia a términos de todas las demás, por lo que probablemente necesitará leer todo esto más de una vez.

calculado vs no calculado (constante)

Los miembros de enumeración pueden calcularse o no. La especificación llama constantes a los miembros no calculados , pero los llamaré no calculados para evitar confusiones con const .

Un miembro de enumeración calculado es aquel cuyo valor no se conoce en tiempo de compilación. Las referencias a los miembros calculados no se pueden insertar, por supuesto. Por el contrario, un miembro de enumeración no calculado es una vez cuyo valor se conoce en tiempo de compilación. Las referencias a miembros no calculados siempre están alineadas.

¿Qué miembros de enumeración se calculan y cuáles no? Primero, todos los miembros de una constenumeración son constantes (es decir, no calculados), como su nombre lo indica. Para una enumeración no constante, depende de si estás mirando un ambiente enumeración (declarar) o una enumeración no ambiental.

Un miembro de a declare enum(es decir, ambiente enum) es constante si y solo si tiene un inicializador. De lo contrario, se calcula. Tenga en cuenta que en a declare enum, solo se permiten inicializadores numéricos. Ejemplo:

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

Por último, los miembros de enumeraciones no constantes no declaradas siempre se consideran calculadas. Sin embargo, sus expresiones de inicialización se reducen a constantes si son computables en tiempo de compilación. Esto significa que los miembros de enumeración que no son constantes nunca están alineados (este comportamiento cambió en TypeScript 1.5, consulte "Cambios en TypeScript" en la parte inferior)

const vs no const

constante

Una declaración de enumeración puede tener el constmodificador. Si una enumeración es const, todas las referencias a sus miembros en línea.

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

las enumeraciones const no producen un objeto de búsqueda cuando se compilan. Por esta razón, es un error hacer referencia Fooen el código anterior, excepto como parte de una referencia de miembro. Ningún Fooobjeto estará presente en tiempo de ejecución.

no constante

Si una declaración de enumeración no tiene el constmodificador, las referencias a sus miembros están en línea solo si el miembro no está calculado. Una enumeración no constante y no declarada producirá un objeto de búsqueda.

declarar (ambiente) vs no declarar

Un prefacio importante es que declareen TypeScript tiene un significado muy específico: este objeto existe en otro lugar . Es para describir objetos existentes . Usar declarepara definir objetos que en realidad no existen puede tener malas consecuencias; los exploraremos más tarde.

declarar

A declare enumno emitirá un objeto de búsqueda. Las referencias a sus miembros están en línea si esos miembros se calculan (consulte más arriba sobre calculados frente a no calculados).

Es importante señalar que otras formas de referencia a un declare enum están permitidos, por ejemplo, este código es no un error de compilación, pero se fallará en tiempo de ejecución:

// 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 error se incluye en la categoría de "No mientas al compilador". Si no tiene un objeto nombrado Fooen tiempo de ejecución, ¡no escriba declare enum Foo!

A declare const enumno es diferente de a const enum, excepto en el caso de --preserveConstEnums (ver más abajo).

no declarar

Una enumeración no declarada produce un objeto de búsqueda si no lo es const. Inlining se describe arriba.

--preserveConstEnums bandera

Esta bandera tiene exactamente un efecto: las enumeraciones const no declaradas emitirán un objeto de búsqueda. Inlining no se ve afectado. Esto es útil para depurar.


Errores comunes

El error más común es usar a declare enumcuando sería más apropiado enumo regular const enum. Una forma común es 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]);
}

Recuerda la regla de oro: nunca declarecosas que en realidad no existen . Úselo const enumsi siempre desea alinear o enumsi desea el objeto de búsqueda.


Cambios en TypeScript

Entre TypeScript 1.4 y 1.5, hubo un cambio en el comportamiento (ver https://github.com/Microsoft/TypeScript/issues/2183 ) para hacer que todos los miembros de enumeraciones no constantes no declaradas se traten como computados, incluso si se inicializan explícitamente con un literal. Esto "deshace al bebé", por así decirlo, hace que el comportamiento de alineación sea más predecible y separa más claramente el concepto de const enumregular enum. Antes de este cambio, los miembros no calculados de enumeraciones no constantes se insertaron de forma más agresiva.

Ryan Cavanaugh
fuente
6
Una respuesta realmente asombrosa. Me aclaró muchas cosas, no solo enumeraciones.
Clark
1
Desearía poder votar por ti más de una vez ... no sabía sobre ese cambio radical. En una versión semántica adecuada, esto podría considerarse un aumento en la versión principal: - /
mfeineis
Una comparación muy útil de los distintos enumtipos, ¡gracias!
Marius Schulz
@Ryan esto es muy útil, ¡gracias! Ahora solo necesitamos Web Essentials 2015 para producir el adecuado constpara los tipos de enumeración declarados.
styfle
19
Esta respuesta parece entrar en gran detalle explicando una situación en 1.4, y luego al final dice "pero 1.5 cambió todo eso y ahora es mucho más simple". Suponiendo que entiendo las cosas correctamente, esta organización se volverá cada vez más inapropiada a medida que esta respuesta envejezca: recomiendo encarecidamente poner primero la situación actual más simple , y solo después de eso, decir "pero si estás usando 1.4 o anterior, las cosas son un poco más complicadas ".
KRyan
33

Aquí están sucediendo algunas cosas. Vayamos caso por caso.

enumeración

enum Cheese { Brie, Cheddar }

Primero, una simple enumeración. Cuando se compila en JavaScript, esto emitirá una tabla de búsqueda.

La tabla de búsqueda se ve así:

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

Luego, cuando lo tiene Cheese.Brieen TypeScript, emite Cheese.Brieen JavaScript que evalúa a 0. Cheese[0]emite Cheese[0]y en realidad evalúa a "Brie".

const enum

const enum Bread { Rye, Wheat }

¡No se emite ningún código para esto! Sus valores están en línea. Lo siguiente emite el valor 0 en sí mismo en JavaScript:

Bread.Rye
Bread['Rye']

const enumLa inserción de s puede resultar útil por motivos de rendimiento.

¿Pero de qué Bread[0]? Esto generará un error en tiempo de ejecución y su compilador debería detectarlo. No hay una tabla de búsqueda y el compilador no está incluido aquí.

Tenga en cuenta que en el caso anterior, la marca --preserveConstEnums hará que Bread emita una tabla de búsqueda. Sin embargo, sus valores aún estarán en línea.

declarar enumeración

Al igual que con otros usos de declare, declareno emite código y espera que haya definido el código real en otro lugar. Esto no emite una tabla de búsqueda:

declare enum Wine { Red, Wine }

Wine.Red emite Wine.Red en JavaScript, pero no habrá ninguna tabla de búsqueda de Wine a la que hacer referencia, por lo que es un error a menos que lo haya definido en otro lugar.

declare const enum

Esto no emite una tabla de búsqueda:

declare const enum Fruit { Apple, Pear }

¡Pero lo hace en línea! Fruit.Appleemite 0. Pero nuevamente se Fruit[0]producirá un error en tiempo de ejecución porque no está en línea y no hay una tabla de búsqueda.

He escrito esto en este patio de recreo. Recomiendo jugar allí para comprender qué TypeScript emite qué JavaScript.

Kat
fuente
1
Recomiendo actualizar esta respuesta: A partir de Typecript 3.3.3, Bread[0]arroja un error de compilador: "Solo se puede acceder a un miembro de enum const usando un literal de cadena".
chharvey
1
Hm ... ¿es eso diferente de lo que dice la respuesta? "¿Pero qué pasa con Bread [0]? Esto generará un error en tiempo de ejecución y su compilador debería detectarlo. No hay una tabla de búsqueda y el compilador no está en línea aquí".
Kat