Wie funktionieren die verschiedenen Enum-Varianten in TypeScript?


116

TypeScript bietet verschiedene Möglichkeiten, eine Aufzählung zu definieren:

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

Wenn ich versuche, einen Wert von Gammazur Laufzeit zu verwenden, wird eine Fehlermeldung angezeigt, da diese Gammanicht definiert ist. Dies ist jedoch nicht der Fall für Deltaoder Alpha? Was bedeutet constoder declarebedeutet dies auf den Erklärungen hier?

Es gibt auch ein preserveConstEnumsCompiler-Flag - wie interagiert dies mit diesen?


1
Ich habe gerade einen Artikel darüber geschrieben , obwohl es mehr mit dem Vergleich von const mit nicht const enums zu tun hat
joelmdev

Antworten:


246

Aufzählungen in TypeScript haben vier verschiedene Aspekte, die Sie beachten müssen. Zunächst einige Definitionen:

"Suchobjekt"

Wenn Sie diese Aufzählung schreiben:

enum Foo { X, Y }

TypeScript gibt das folgende Objekt aus:

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

Ich werde dies als Suchobjekt bezeichnen . Sein Zweck ist zweifach: als Abbildung von dienen Strings zu Zahlen , zum Beispiel beim Schreiben Foo.Xoder Foo['X'], und als eine Abbildung von dienen Zahlen zu Strings . Diese umgekehrte Zuordnung ist nützlich für Debugging- oder Protokollierungszwecke - Sie haben häufig den Wert 0oder 1und möchten die entsprechende Zeichenfolge "X"oder erhalten "Y".

"deklarieren" oder " umgebungs "

In TypeScript können Sie Dinge "deklarieren", über die der Compiler Bescheid wissen sollte, für die er jedoch keinen Code ausgibt. Dies ist nützlich, wenn Sie Bibliotheken wie jQuery haben, die ein Objekt definieren (z. B. $), zu dem Sie Typinformationen wünschen, aber keinen vom Compiler erstellten Code benötigen. Die Spezifikation und andere Dokumentationen beziehen sich auf Erklärungen, die auf diese Weise abgegeben wurden und sich in einem "Umgebungs" -Kontext befinden. Es ist wichtig zu beachten, dass alle Deklarationen in einer .d.tsDatei "ambient" sind ( declareje nach Deklarationstyp entweder einen expliziten Modifikator erforderlich oder implizit).

"Inlining"

Aus Gründen der Leistung und der Codegröße ist es häufig vorzuziehen, beim Kompilieren einen Verweis auf ein Enum-Mitglied durch sein numerisches Äquivalent zu ersetzen:

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

Die Spezifikation nennt diese Substitution , ich werde sie Inlining nennen, weil sie cooler klingt. Manchmal möchten Sie nicht, dass Enum-Mitglieder eingefügt werden, z. B. weil sich der Enum-Wert in einer zukünftigen Version der API möglicherweise ändert.


Enums, wie funktionieren sie?

Lassen Sie uns dies nach jedem Aspekt einer Aufzählung aufschlüsseln. Leider wird in jedem dieser vier Abschnitte auf Begriffe aus allen anderen Abschnitten verwiesen, sodass Sie diese ganze Sache wahrscheinlich mehr als einmal lesen müssen.

berechnet gegen nicht berechnet (konstant)

Enum-Mitglieder können entweder berechnet werden oder nicht. Die Spezifikation nennt nicht berechnete Mitglieder konstant , aber ich werde sie nicht berechnet nennen , um Verwechslungen mit const zu vermeiden .

Ein berechnetes Enum-Mitglied ist eines, dessen Wert zur Kompilierungszeit nicht bekannt ist. Verweise auf berechnete Mitglieder können natürlich nicht eingefügt werden. Umgekehrt ist ein nicht berechnetes Enum-Mitglied einmal vorhanden, dessen Wert zur Kompilierungszeit bekannt ist . Verweise auf nicht berechnete Mitglieder werden immer eingefügt.

Welche Enum-Mitglieder werden berechnet und welche nicht berechnet? Erstens sind alle Mitglieder einer constAufzählung konstant (dh nicht berechnet), wie der Name schon sagt. Bei einer nicht konstanten Aufzählung hängt es davon ab, ob Sie eine Umgebungsaufzählung (deklarieren) oder eine nicht umgebungsbezogene Aufzählung betrachten.

Ein Mitglied einer declare enum(dh Umgebungsaufzählung) ist genau dann konstant, wenn es einen Initialisierer hat. Andernfalls wird es berechnet. Beachten Sie, dass in a declare enumnur numerische Initialisierer zulässig sind. Beispiel:

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

Schließlich gelten Mitglieder von nicht deklarierten Nicht-Konstanten-Aufzählungen immer als berechnet. Ihre initialisierenden Ausdrücke werden jedoch auf Konstanten reduziert, wenn sie zur Kompilierungszeit berechenbar sind. Dies bedeutet, dass nicht konstante Enum-Mitglieder niemals inline sind (dieses Verhalten wurde in TypeScript 1.5 geändert, siehe "Änderungen in TypeScript" unten).

const vs non-const

const

Eine Enum-Deklaration kann den constModifikator haben. Wenn eine Aufzählung vorhanden ist const, werden alle Verweise auf ihre Mitglieder eingefügt.

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

const enums erzeugen beim Kompilieren kein Lookup-Objekt. Aus diesem Grund ist es ein Fehler, Fooim obigen Code zu referenzieren , außer als Teil einer Mitgliedsreferenz. Zur FooLaufzeit ist kein Objekt vorhanden.

non-const

Wenn eine Enum-Deklaration nicht über den constModifikator verfügt, werden Verweise auf ihre Mitglieder nur dann eingefügt, wenn das Mitglied nicht berechnet wurde. Eine nicht konstante, nicht deklarierte Aufzählung erzeugt ein Suchobjekt.

deklarieren (Umgebungs) vs nicht deklarieren

Ein wichtiges Vorwort ist, dass declareTypeScript eine ganz bestimmte Bedeutung hat: Dieses Objekt existiert woanders . Es dient zur Beschreibung vorhandener Objekte. Das declareDefinieren von Objekten, die tatsächlich nicht existieren, kann schlimme Folgen haben. Wir werden diese später untersuchen.

erklären

A declare enumgibt kein Suchobjekt aus. Verweise auf seine Mitglieder werden eingefügt, wenn diese Mitglieder berechnet werden (siehe oben zu berechnet oder nicht berechnet).

Es ist wichtig , dass andere Formen der Bezugnahme auf eine zu beachten declare enum sind erlaubt, zum Beispiel ist dieser Code nicht ein Übersetzungsfehler , sondern wird zur Laufzeit fehlschlagen:

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

Dieser Fehler fällt unter die Kategorie "Lüg den Compiler nicht an". Wenn Sie Foozur Laufzeit kein Objekt mit dem Namen haben, schreiben Sie nicht declare enum Foo!

A unterscheidet declare const enumsich nicht von a const enum, außer im Fall von --preserveConstEnums (siehe unten).

nicht deklarieren

Eine nicht deklarierte Aufzählung erzeugt ein Suchobjekt, wenn dies nicht der Fall ist const. Inlining ist oben beschrieben.

--preserveConstEnums Flag

Dieses Flag hat genau einen Effekt: Nicht deklarierte Konstanten geben ein Suchobjekt aus. Inlining ist nicht betroffen. Dies ist nützlich zum Debuggen.


Häufige Fehler

Der häufigste Fehler ist die Verwendung eines, declare enumwenn ein regulärer enumoder const enumangemessener wäre. Eine übliche Form ist folgende:

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

Denken Sie an die goldene Regel: Niemals declareDinge, die es eigentlich nicht gibt . Verwenden const enumSie diese Option, wenn Sie immer Inlining möchten oder enumwenn Sie das Suchobjekt möchten.


Änderungen in TypeScript

Zwischen TypeScript 1.4 und 1.5 wurde das Verhalten geändert (siehe https://github.com/Microsoft/TypeScript/issues/2183 ), sodass alle Mitglieder von nicht deklarierten Nicht-Konstanten-Aufzählungen als berechnet behandelt werden, auch wenn Sie werden explizit mit einem Literal initialisiert. Dieses „Auftrennung aufzuheben , das Baby“, so zu sprechen, so dass das inlining Verhalten berechenbarer und sauberer das Konzept der Trennung const enumvon regelmäßigen enum. Vor dieser Änderung wurden nicht berechnete Mitglieder von Nicht-Konstanten-Enums aggressiver eingefügt.


6
Eine wirklich tolle Antwort. Es hat so viele Dinge für mich geklärt, nicht nur Aufzählungen.
Clark

1
Ich wünschte, ich könnte dich mehr als einmal abstimmen ... wusste nichts über diese bahnbrechende Veränderung. In der richtigen semantischen Versionierung könnte dies als eine
Beeinträchtigung

Ein sehr hilfreicher Vergleich der verschiedenen enumTypen, danke!
Marius Schulz

@ Ryan das ist sehr hilfreich, danke! Jetzt brauchen wir nur noch Web Essentials 2015 , um das richtige constfür deklarierte Aufzählungstypen zu erstellen .
Styfle

19
Diese Antwort scheint sehr detailliert zu sein und erklärt eine Situation in 1.4. Am Ende steht dann: "Aber 1.5 hat das alles geändert und jetzt ist es viel einfacher." Vorausgesetzt, ich verstehe die Dinge richtig, wird diese Organisation mit zunehmendem Alter dieser Antwort immer unangemessener: Ich empfehle dringend, die einfachere, aktuelle Situation an die erste Stelle zu setzen und erst danach zu sagen: „Aber wenn Sie 1.4 oder früher verwenden, Dinge sind etwas komplizierter. "
KRyan

33

Hier sind ein paar Dinge los. Gehen wir von Fall zu Fall.

Aufzählung

enum Cheese { Brie, Cheddar }

Erstens eine einfache alte Aufzählung. Bei der Kompilierung mit JavaScript wird eine Nachschlagetabelle ausgegeben.

Die Nachschlagetabelle sieht folgendermaßen aus:

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

Wenn Sie dann Cheese.Briein TypeScript haben, wird es Cheese.Briein JavaScript ausgegeben, das mit 0 ausgewertet wird . Es wird Cheese[0]ausgegeben Cheese[0]und tatsächlich mit ausgewertet "Brie".

const enum

const enum Bread { Rye, Wheat }

Hierfür wird eigentlich kein Code ausgegeben! Seine Werte sind inline. Folgendes gibt den Wert 0 selbst in JavaScript aus:

Bread.Rye
Bread['Rye']

const enumDas Inlining von s kann aus Leistungsgründen nützlich sein.

Aber was ist mit Bread[0]? Dies tritt zur Laufzeit auf und Ihr Compiler sollte es abfangen. Es gibt keine Nachschlagetabelle und der Compiler wird hier nicht inline.

Beachten Sie, dass im obigen Fall das Flag --preserveConstEnums dazu führt, dass Bread eine Nachschlagetabelle ausgibt. Seine Werte werden jedoch weiterhin eingefügt.

enum deklarieren

Wie bei anderen Verwendungen declare, declareemittiert keinen Code und erwartet Sie den eigentlichen Code an anderer Stelle definiert haben. Dies gibt keine Nachschlagetabelle aus:

declare enum Wine { Red, Wine }

Wine.Redwird Wine.Redin JavaScript ausgegeben, aber es gibt keine Wine-Nachschlagetabelle, auf die verwiesen werden kann. Es handelt sich also um einen Fehler, es sei denn, Sie haben ihn an anderer Stelle definiert.

deklariere const enum

Dies gibt keine Nachschlagetabelle aus:

declare const enum Fruit { Apple, Pear }

Aber es macht inline! Fruit.Applegibt 0 aus. Fruit[0]Wird aber zur Laufzeit erneut auftreten, da es nicht inline ist und keine Nachschlagetabelle vorhanden ist.

Ich habe das auf diesem Spielplatz geschrieben. Ich empfehle, dort zu spielen, um zu verstehen, welches TypeScript welches JavaScript ausgibt.


1
Ich empfehle, diese Antwort zu aktualisieren: Ab Typescript 3.3.3 wird Bread[0]ein Compilerfehler ausgegeben : "Auf ein const enum-Mitglied kann nur mit einem Zeichenfolgenliteral zugegriffen werden."
Chharvey

1
Hm ... unterscheidet sich das von dem, was die Antwort sagt? "Aber was ist mit Bread [0]? Dies wird zur Laufzeit fehlerhaft und Ihr Compiler sollte es abfangen. Es gibt keine Nachschlagetabelle und der Compiler wird hier nicht inline."
Kat
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.