Comment fonctionnent les différentes variantes d'énumération dans TypeScript?

116

TypeScript a un tas de façons différentes de définir une énumération:

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 j'essaie d'utiliser une valeur de Gammaau moment de l'exécution, j'obtiens une erreur car Gamman'est pas définie, mais ce n'est pas le cas pour Deltaou Alpha? Que signifie constou que declaresignifient les déclarations ici?

Il y a aussi un preserveConstEnumsindicateur de compilateur - comment cela interagit-il avec ceux-ci?

Ryan Cavanaugh
la source
1
Je viens d'écrire un article à ce sujet , bien que cela ait plus à voir avec la comparaison des enums const et non const
joelmdev

Réponses:

247

Il y a quatre aspects différents des énumérations dans TypeScript dont vous devez être conscient. Tout d'abord, quelques définitions:

"objet de recherche"

Si vous écrivez cette énumération:

enum Foo { X, Y }

TypeScript émettra l'objet suivant:

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

J'appellerai cela l'objet de recherche . Son objectif est double: servir de correspondance entre les chaînes et les nombres , par exemple lors de l'écriture Foo.Xou Foo['X'], et servir de correspondance entre les nombres et les chaînes . Ce mappage inversé est utile à des fins de débogage ou de journalisation - vous aurez souvent la valeur 0ou 1et souhaitez obtenir la chaîne correspondante "X"ou "Y".

"déclarer" ou " ambiant "

Dans TypeScript, vous pouvez «déclarer» des choses que le compilateur devrait connaître, mais pas réellement pour lesquelles émettre du code. Ceci est utile lorsque vous avez des bibliothèques comme jQuery qui définissent un objet (par exemple $) sur lequel vous voulez des informations de type, mais qui n'ont pas besoin de code créé par le compilateur. La spécification et d'autres documents se réfèrent aux déclarations faites de cette façon comme étant dans un contexte «ambiant»; il est important de noter que toutes les déclarations dans un .d.tsfichier sont "ambiantes" (soit nécessitant un declaremodificateur explicite, soit l'avoir implicitement, selon le type de déclaration).

"inlining"

Pour des raisons de performances et de taille de code, il est souvent préférable de remplacer une référence à un membre enum par son équivalent numérique lors de la compilation:

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

La spécification appelle cette substitution , je l'appellerai inlining car elle sonne plus cool. Parfois, vous ne souhaiterez pas que les membres d'énumération soient insérés, par exemple parce que la valeur d'énumération pourrait changer dans une version future de l'API.


Enums, comment fonctionnent-ils?

Décomposons cela par chaque aspect d'une énumération. Malheureusement, chacune de ces quatre sections fera référence aux termes de toutes les autres, vous aurez donc probablement besoin de lire tout cela plus d'une fois.

calculée vs non calculée (constante)

Les membres d'énumération peuvent être calculés ou non. La spécification appelle les membres non calculés constants , mais je les appellerai non calculés pour éviter toute confusion avec const .

Un membre enum calculé est un membre dont la valeur n'est pas connue au moment de la compilation. Les références aux membres calculés ne peuvent pas être insérées, bien sûr. Inversement, un membre enum non calculé est une fois dont la valeur est connue au moment de la compilation. Les références aux membres non calculés sont toujours insérées.

Quels membres d'énumération sont calculés et lesquels ne sont pas calculés? Premièrement, tous les membres d'une consténumération sont constants (c'est-à-dire non calculés), comme son nom l'indique. Pour une énumération non-const, cela dépend si vous regardez une énumération ambiante (déclarée) ou une énumération non ambiante.

Un membre de a declare enum(ie ambient enum) est constant si et seulement s'il a un initialiseur. Sinon, il est calculé. Notez que dans a declare enum, seuls les initialiseurs numériques sont autorisés. Exemple:

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

Enfin, les membres des énumérations non déclarées non const sont toujours considérés comme calculés. Cependant, leurs expressions d'initialisation sont réduites à des constantes si elles sont calculables au moment de la compilation. Cela signifie que les membres d'énumération non const ne sont jamais incorporés (ce comportement a changé dans TypeScript 1.5, voir «Modifications dans TypeScript» en bas)

const vs non-const

const

Une déclaration enum peut avoir le constmodificateur. Si une énumération est const, toutes les références à ses membres sont incorporées.

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

Les énumérations const ne produisent pas d'objet de recherche lors de la compilation. Pour cette raison, il est erroné de faire référence Foodans le code ci-dessus, sauf dans le cadre d'une référence de membre. Aucun Fooobjet ne sera présent lors de l'exécution.

non const

Si une déclaration d'énumération n'a pas le constmodificateur, les références à ses membres sont insérées uniquement si le membre n'est pas calculé. Une énumération non const et non déclarée produira un objet de recherche.

déclarer (ambiant) vs non-déclarer

Une préface importante est que declaredans TypeScript a une signification très spécifique: cet objet existe ailleurs . C'est pour décrire des objets existants . Utiliser declarepour définir des objets qui n'existent pas réellement peut avoir de mauvaises conséquences; nous les explorerons plus tard.

déclarer

A declare enumn'émettra pas d'objet de recherche. Les références à ses membres sont insérées si ces membres sont calculés (voir ci-dessus sur calculé vs non calculé).

Il est important de noter que d' autres formes de référence à un declare enum sont autorisés, par exemple , ce code est pas une erreur de compilation mais va échouer à l' exécution:

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

Cette erreur relève de la catégorie "Ne mentez pas au compilateur". Si vous n'avez pas d'objet nommé Fooau moment de l'exécution, n'écrivez pas declare enum Foo!

A declare const enumn'est pas différent de a const enum, sauf dans le cas de --preserveConstEnums (voir ci-dessous).

non-déclarer

Une énumération non déclarée produit un objet de recherche si ce n'est pas le cas const. L'intégration est décrite ci-dessus.

--preserveConstEnums indicateur

Cet indicateur a exactement un effet: les énumérations const non déclarées émettront un objet de recherche. L'intégration n'est pas affectée. Ceci est utile pour le débogage.


Erreurs courantes

L'erreur la plus courante est d'utiliser un declare enumquand un régulier enumou const enumserait plus approprié. Une forme courante est la suivante:

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

Rappelez-vous la règle d'or: jamais des declarechoses qui n'existent pas . À utiliser const enumsi vous voulez toujours l'incrustation ou enumsi vous voulez l'objet de recherche.


Changements dans TypeScript

Entre TypeScript 1.4 et 1.5, il y a eu un changement dans le comportement (voir https://github.com/Microsoft/TypeScript/issues/2183 ) pour que tous les membres d'énumérations non déclarées non const soient traités comme calculés, même si ils sont explicitement initialisés avec un littéral. Ce «débris le bébé», pour ainsi dire, rend le comportement en ligne plus prévisible et sépare plus proprement le concept de const enumrégulier enum. Avant ce changement, les membres non calculés des non-const enums étaient intégrés de manière plus agressive.

Ryan Cavanaugh
la source
6
Une réponse vraiment géniale. Cela a éclairci tellement de choses pour moi, pas seulement les énumérations.
Clark
1
J'aurais aimé pouvoir vous voter plus d'une fois ... Je ne savais pas ce changement révolutionnaire. Dans une version sémantique appropriée, cela pourrait être considéré comme une bosse à la version majeure: - /
mfeineis
Une comparaison très utile des différents enumtypes, merci!
Marius Schulz
@Ryan c'est très utile, merci! Maintenant, nous avons juste besoin de Web Essentials 2015 pour produire le bon constpour les types enum déclarés.
styfle
19
Cette réponse semble entrer dans les détails pour expliquer une situation dans la version 1.4, puis à la toute fin, elle dit «mais la version 1.5 a tout changé et maintenant c'est beaucoup plus simple». En supposant que je comprends les choses correctement, cette organisation deviendra de plus en plus inappropriée à mesure que cette réponse vieillira: je recommande fortement de mettre la situation actuelle plus simple en premier , et seulement après cela en disant "mais si vous utilisez la version 1.4 ou antérieure, les choses sont un peu plus compliqués.
KRyan
33

Il se passe plusieurs choses ici. Allons au cas par cas.

énumération

enum Cheese { Brie, Cheddar }

Tout d'abord, une simple énumération ancienne. Une fois compilé en JavaScript, cela émettra une table de recherche.

La table de recherche ressemble à ceci:

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

Ensuite, lorsque vous avez Cheese.Briedans TypeScript, il émet Cheese.Brieen JavaScript qui évalue à 0. Cheese[0]émet Cheese[0]et évalue réellement à "Brie".

const énumération

const enum Bread { Rye, Wheat }

Aucun code n'est réellement émis pour cela! Ses valeurs sont intégrées. Les éléments suivants émettent la valeur 0 elle-même en JavaScript:

Bread.Rye
Bread['Rye']

const enums 'inlining peut être utile pour des raisons de performances.

Mais qu'en est-il Bread[0]? Cela provoquera une erreur au moment de l'exécution et votre compilateur devrait l'attraper. Il n'y a pas de table de recherche et le compilateur n'est pas en ligne ici.

Notez que dans le cas ci-dessus, l'indicateur --preserveConstEnums provoquera l'émission d'une table de recherche par Bread. Cependant, ses valeurs seront toujours intégrées.

déclarer enum

Comme pour les autres utilisations de declare, declaren'émet aucun code et s'attend à ce que vous ayez défini le code réel ailleurs. Cela n'émet aucune table de recherche:

declare enum Wine { Red, Wine }

Wine.Redémet Wine.Reden JavaScript, mais il n'y aura pas de table de recherche Wine à référencer, c'est donc une erreur à moins que vous ne l'ayez défini ailleurs.

declare const enum

Cela n'émet aucune table de recherche:

declare const enum Fruit { Apple, Pear }

Mais c'est en ligne! Fruit.Appleémet 0. Mais encore une fois, il y Fruit[0]aura une erreur lors de l'exécution car il n'est pas en ligne et il n'y a pas de table de recherche.

J'ai écrit ceci dans ce terrain de jeu. Je recommande d'y jouer pour comprendre quel TypeScript émet quel JavaScript.

Kat
la source
1
Je recommande de mettre à jour cette réponse: à partir de Typescript 3.3.3, Bread[0]génère une erreur de compilation: «Un membre const enum n'est accessible qu'à l'aide d'une chaîne littérale.»
chharvey
1
Hm ... est-ce différent de ce que dit la réponse? "Mais qu'en est-il de Bread [0]? Cela provoquera une erreur lors de l'exécution et votre compilateur devrait l'attraper. Il n'y a pas de table de recherche et le compilateur n'est pas en ligne ici."
Kat