Come funzionano le diverse varianti di enumerazione in TypeScript?


116

TypeScript ha molti modi diversi per definire un'enumerazione:

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 provo a utilizzare un valore da Gammain fase di esecuzione, ottengo un errore perché Gammanon è definito, ma non è il caso di Deltao Alpha? Cosa significa consto declaresignifica sulle dichiarazioni qui?

C'è anche un preserveConstEnumsflag del compilatore: come interagisce con questi?


1
Ho appena scritto un articolo su questo , anche se ha più a che fare con il confronto tra const e non const enums
joelmdev

Risposte:


247

Ci sono quattro diversi aspetti da enumerare in TypeScript di cui devi essere a conoscenza. Innanzitutto, alcune definizioni:

"oggetto di ricerca"

Se scrivi questa enumerazione:

enum Foo { X, Y }

TypeScript emetterà il seguente oggetto:

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

Mi riferirò a questo come l'oggetto di ricerca . Il suo scopo è duplice: servire da mappatura da stringhe a numeri , ad esempio quando si scrive Foo.Xo Foo['X'], e da mappare da numeri a stringhe . Questa mappatura inversa è utile per scopi di debug o registrazione: spesso avrai il valore 0o 1e vorrai ottenere la stringa corrispondente "X"o "Y".

"declare" o " ambient "

In TypeScript, puoi "dichiarare" cose che il compilatore dovrebbe conoscere, ma non emettere effettivamente codice per. Ciò è utile quando si dispone di librerie come jQuery che definiscono un oggetto (ad esempio $) di cui si desiderano informazioni sul tipo, ma non è necessario alcun codice creato dal compilatore. Le specifiche e altra documentazione si riferiscono a dichiarazioni fatte in questo modo come se fossero in un contesto "ambientale"; è importante notare che tutte le dichiarazioni in un .d.tsfile sono "ambientali" (o richiedono un declaremodificatore esplicito o lo hanno implicitamente, a seconda del tipo di dichiarazione).

"Inlining"

Per motivi di prestazioni e dimensione del codice, è spesso preferibile avere un riferimento a un membro enum sostituito dal suo equivalente numerico quando viene compilato:

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

La specifica chiama questa sostituzione , la chiamerò inlining perché suona più interessante. A volte lo farai non desidera che i membri enum da inline, ad esempio perché il valore enum potrebbe cambiare in una futura versione di API.


Enum, come funzionano?

Analizziamolo per ogni aspetto di un enum. Sfortunatamente, ciascuna di queste quattro sezioni farà riferimento a termini di tutte le altre, quindi probabilmente dovrai leggere l'intera cosa più di una volta.

calcolato vs non calcolato (costante)

I membri di enum possono essere calcolati o meno. La specifica chiama costanti membri non calcolati , ma li chiamerò non calcolati per evitare confusione con const .

Un membro enum calcolato è un membro il cui valore non è noto in fase di compilazione. I riferimenti ai membri calcolati non possono essere inseriti in linea, ovviamente. Al contrario, un membro enum non calcolato è una volta il cui valore è noto in fase di compilazione. I riferimenti a membri non calcolati sono sempre inline.

Quali membri di enum vengono calcolati e quali non vengono calcolati? Primo, tutti i membri di un constenum sono costanti (cioè non calcolati), come suggerisce il nome. Per un'enumerazione non const, dipende dal fatto che si stia osservando un'enumerazione ambientale (dichiarare) o un'enumerazione non ambientale.

Un membro di a declare enum(es. Enum ambientale) è costante se e solo se ha un inizializzatore. Altrimenti, viene calcolato. Notare che in a declare enum, sono consentiti solo inizializzatori numerici. Esempio:

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

Infine, i membri di enumerazioni non dichiarate non cost vengono sempre considerati calcolati. Tuttavia, le loro espressioni di inizializzazione sono ridotte a costanti se sono calcolabili in fase di compilazione. Ciò significa che i membri non-const enum non sono mai inline (questo comportamento è cambiato in TypeScript 1.5, vedi "Modifiche in TypeScript" in fondo)

const vs non const

const

Una dichiarazione enum può avere il constmodificatore. Se un'enumerazione è const, tutti i riferimenti ai suoi membri sono inline.

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

const enums non producono un oggetto di ricerca quando vengono compilati. Per questo motivo, è un errore fare riferimento Foonel codice precedente tranne come parte di un riferimento a un membro. Nessun Foooggetto sarà presente in fase di esecuzione.

non-const

Se una dichiarazione enum non ha il constmodificatore, i riferimenti ai suoi membri sono inline solo se il membro non è calcolato. Un enum non-const, non-declare produrrà un oggetto di ricerca.

declare (ambient) vs non-declare

Una premessa importante è che declarein TypeScript ha un significato molto specifico: questo oggetto esiste da qualche altra parte . Serve per descrivere oggetti esistenti . Usare declareper definire oggetti che in realtà non esistono può avere conseguenze negative; li esploreremo più tardi.

dichiarare

A declare enumnon emetterà un oggetto di ricerca. I riferimenti ai suoi membri sono inline se questi membri sono calcolati (vedi sopra su calcolati vs non calcolati).

E 'importante notare che altre forme di riferimento ad una declare enum sono permessi, ad esempio, questo codice è non è un errore di compilazione, ma sarà fallire in fase di esecuzione:

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

Questo errore rientra nella categoria "Non mentire al compilatore". Se non hai un oggetto denominato Fooin fase di esecuzione, non scrivere declare enum Foo!

A declare const enumnon è diverso da a const enum, tranne nel caso di --preserveConstEnums (vedi sotto).

non dichiarare

Se non lo è, un'enumerazione non dichiarata produce un oggetto di ricerca const. L'inlining è descritto sopra.

--preserveConstEnums flag

Questo flag ha esattamente un effetto: le enumerazioni const non dichiarate genereranno un oggetto di ricerca. L'inlining non è interessato. Questo è utile per il debug.


Errori comuni

L'errore più comune è usare un declare enumquando un normale enumo const enumsarebbe più appropriato. Una forma comune è questa:

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

Ricorda la regola d'oro: mai declarecose che in realtà non esistono . Usalo const enumse vuoi sempre inlining o enumse vuoi l'oggetto di ricerca.


Modifiche in TypeScript

Tra TypeScript 1.4 e 1.5, è stata apportata una modifica al comportamento (vedere https://github.com/Microsoft/TypeScript/issues/2183 ) per fare in modo che tutti i membri di enumerazioni non dichiarate non cost vengano trattati come calcolati, anche se sono esplicitamente inizializzati con un letterale. Questo "separare il bambino", per così dire, rende il comportamento in linea più prevedibile e separa in modo più netto il concetto di const enumregolare enum. Prima di questa modifica, i membri non calcolati di enumerazioni non cost venivano inseriti in modo più aggressivo.


6
Una risposta davvero fantastica. Mi ha chiarito tante cose, non solo enumerazioni.
Clark

1
Vorrei poterti votare più di una volta ... non sapevo di quel cambiamento radicale. In una corretta versione semantica questo potrebbe essere considerato un bernoccolo alla versione principale: - /
mfeineis

Un confronto molto utile dei vari enumtipi, grazie!
Marius Schulz

@ Ryan questo è molto utile, grazie! Ora abbiamo solo bisogno di Web Essentials 2015 per produrre il corretto constper i tipi enum dichiarati.
stile

19
Questa risposta sembra entrare nei dettagli spiegando una situazione in 1.4, e poi alla fine dice "ma 1.5 ha cambiato tutto questo e ora è molto più semplice". Supponendo che io capisca le cose correttamente, questa organizzazione diventerà sempre più inappropriata man mano che questa risposta invecchia: consiglio vivamente di mettere prima la situazione più semplice e attuale e solo dopo aver detto "ma se stai usando 1.4 o versioni precedenti, le cose sono un po 'più complicate. "
KRyan

33

Ci sono alcune cose che stanno succedendo qui. Andiamo caso per caso.

enum

enum Cheese { Brie, Cheddar }

Primo, un semplice vecchio enum. Quando viene compilato in JavaScript, emetterà una tabella di ricerca.

La tabella di ricerca ha questo aspetto:

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

Quindi, quando hai Cheese.Briein TypeScript, emette Cheese.Briein JavaScript che restituisce 0. Cheese[0]emette Cheese[0]e restituisce effettivamente "Brie".

const enum

const enum Bread { Rye, Wheat }

Nessun codice viene effettivamente emesso per questo! I suoi valori sono inline. Quanto segue emette il valore 0 stesso in JavaScript:

Bread.Rye
Bread['Rye']

const enums 'inlining potrebbe essere utile per motivi di prestazioni.

Ma di cosa Bread[0]? Questo verrà visualizzato in errore in fase di esecuzione e il compilatore dovrebbe rilevarlo. Non esiste una tabella di ricerca e il compilatore non è in linea qui.

Si noti che nel caso precedente, il flag --preserveConstEnums farà in modo che Bread emetta una tabella di ricerca. I suoi valori saranno comunque integrati.

dichiarare enum

Come con altri usi di declare, declarenon emette codice e si aspetta che tu abbia definito il codice effettivo altrove. Questo non genera alcuna tabella di ricerca:

declare enum Wine { Red, Wine }

Wine.Redemette Wine.Redin JavaScript, ma non ci sarà alcuna tabella di ricerca di Wine a cui fare riferimento, quindi è un errore a meno che tu non l'abbia definito altrove.

dichiarare const enum

Questo non genera alcuna tabella di ricerca:

declare const enum Fruit { Apple, Pear }

Ma funziona in linea! Fruit.Appleemette 0. Ma ancora una volta Fruit[0]verrà visualizzato un errore in fase di esecuzione perché non è inline e non esiste una tabella di ricerca.

L'ho scritto in questo parco giochi. Consiglio di giocare lì per capire quale TypeScript emette quale JavaScript.


1
Consiglio di aggiornare questa risposta: a partire da Typescript 3.3.3, Bread[0]genera un errore del compilatore: "È possibile accedere a un membro const enum solo utilizzando una stringa letterale."
chharvey

1
Hm ... è diverso da quello che dice la risposta? "Ma che dire di Bread [0]? Questo verrà visualizzato in errore in fase di esecuzione e il compilatore dovrebbe rilevarlo. Non esiste una tabella di ricerca e il compilatore non è in linea qui."
Kat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.