Typescript: Étendre les énumérations basées sur des chaînes

Créé le 3 août 2017  ·  68Commentaires  ·  Source: microsoft/TypeScript

Avant les énumérations basées sur des chaînes, beaucoup retombaient sur des objets. L'utilisation d'objets permet également d'étendre les types. Par exemple:

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};

const AdvEvents = {
    ...BasicEvents,
    Pause: "Pause",
    Resume: "Resume"
};

Lors du passage aux énumérations de chaînes, il est impossible d'y parvenir sans redéfinir l'énumération.

Je serais très utile de pouvoir faire quelque chose comme ça:

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

Considérant que les énumérations produites sont des objets, ce ne sera pas trop horrible non plus :

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};
Awaiting More Feedback Suggestion

Commentaire le plus utile

Toutes les solutions de contournement sont intéressantes, mais j'aimerais voir la prise en charge de l'héritage enum à partir de la dactylographie elle-même afin de pouvoir utiliser des vérifications exhaustives aussi simples que possible.

Tous les 68 commentaires

Je viens de jouer un peu avec et il est actuellement possible de faire cette extension en utilisant un objet pour le type étendu, donc cela devrait bien fonctionner :

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
const AdvEvents = {
    ...BasicEvents,
    Pause: "Pause",
    Resume: "Resume"
};

Remarque, vous pouvez vous rapprocher de

enum E {}

enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
function enumerate<T1 extends typeof E, T2 extends typeof E>(e1: T1, e2: T2) {
  enum Events {
    Restart = 'Restart'
  }
  return Events as typeof Events & T1 & T2;
}

const e = enumerate(BasicEvents, AdvEvents);

Une autre option, selon vos besoins, consiste à utiliser un type d'union :

const enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
const enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;

let e: Events = AdvEvents.Pause;

L'inconvénient est que vous ne pouvez pas utiliser Events.Pause ; vous devez utiliser AdvEvents.Pause . Si vous utilisez des énumérations const, c'est probablement correct. Sinon, cela pourrait ne pas être suffisant pour votre cas d'utilisation.

Nous avons besoin de cette fonctionnalité pour les réducteurs Redux fortement typés. Veuillez l'ajouter dans TypeScript.

Une autre solution consiste à ne pas utiliser d'énumérations, mais à utiliser quelque chose qui ressemble à une énumération :

const BasicEvents = {
  Start: 'Start' as 'Start',
  Finish: 'Finish' as 'Finish'
};
type BasicEvents = (typeof BasicEvents)[keyof typeof BasicEvents];

const AdvEvents = {
  ...BasicEvents,
  Pause: 'Pause' as 'Pause',
  Resume: 'Resume' as 'Resume'
};
type AdvEvents = (typeof AdvEvents)[keyof typeof AdvEvents];

Toutes les solutions de contournement sont intéressantes, mais j'aimerais voir la prise en charge de l'héritage enum à partir de la dactylographie elle-même afin de pouvoir utiliser des vérifications exhaustives aussi simples que possible.

Utilisez simplement la classe au lieu de l'énumération.

J'essayais juste ça.

const BasicEvents = {
    Start: 'Start' as 'Start',
    Finish: 'Finish' as 'Finish'
};

type BasicEvents = (typeof BasicEvents)[keyof typeof BasicEvents];

const AdvEvents = {
    ...BasicEvents,
    Pause: 'Pause' as 'Pause',
    Resume: 'Resume' as 'Resume'
};

type AdvEvents = (typeof AdvEvents)[keyof typeof AdvEvents];

type sometype<T extends AdvEvents> =
    T extends typeof AdvEvents.Start ? 'Some String' :
    T extends typeof AdvEvents.Finish ? 'Some Other String' :
    T extends typeof AdvEvents.Pause ? 'Abc' :
    T extends typeof AdvEvents.Resume ? 'Xyz' : never;
type r = sometype<typeof AdvEvents.Finish>;

Il doit y avoir une meilleure façon de faire cela.

Pourquoi n'est-ce pas déjà une fonctionnalité ? Pas de changement radical, un comportement intuitif, plus de 80 personnes qui ont activement recherché et demandé cette fonctionnalité - cela semble être une évidence.

Même réexporter enum à partir d'un fichier différent dans un espace de noms est vraiment bizarre sans étendre les énumérations (et il est impossible de réexporter l'énumération d'une manière c'est toujours enum et non objet et type):

import { Foo as _Foo } from './Foo';

namespace Bar
{
    enum Foo extends _Foo {} // nope, doesn't work

    const Foo = _Foo;
    type Foo = _Foo;
}

Bar.Foo // actually not an enum

obrazek

+1
Utilise actuellement une solution de contournement, mais cela devrait être une fonctionnalité d'énumération native.

J'ai parcouru ce problème pour voir si quelqu'un a posé la question suivante. (Ne semble pas.)

Depuis le PO :

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

Les gens s'attendraient-ils à ce que AdvEvents puisse être attribué à BasicEvents ? (Comme c'est, par exemple, le cas avec extends pour les classes.)

Si oui, dans quelle mesure cela correspond-il au fait que les types enum sont censés être finaux et impossibles à étendre?

@masak excellent point. La fonctionnalité que les gens veulent ici n'est certainement pas comme la normale extends . BasicEvents devrait être assignable à AdvEvents , et non l'inverse. Normal extends affine un autre type pour être plus spécifique, et dans ce cas, nous voulons élargir l'autre type pour ajouter plus de valeurs, donc toute syntaxe personnalisée pour cela ne devrait probablement pas utiliser le mot-clé extends , ou du moins ne pas utiliser la syntaxe enum A extends B { .

Sur cette note, j'ai aimé la suggestion de propagation pour cela de OP.

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

Parce que la propagation porte déjà l'attente que l'original soit cloné en surface dans une copie non connectée.

BasicEvents devrait être assignable à AdvEvents , et non l'inverse.

Je peux voir comment cela pourrait être vrai dans tous les cas, mais je ne suis pas sûr que cela devrait être vrai dans tous les cas, si vous voyez ce que je veux dire. On dirait que cela dépendra du domaine et dépendra de la raison pour laquelle ces valeurs d'énumération ont été copiées.

J'ai pensé un peu plus aux solutions de contournement, et en travaillant sur https://github.com/Microsoft/TypeScript/issues/17592#issuecomment -331491147 , vous pouvez faire un peu mieux en définissant également Events dans la valeur espacer:

enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;
const Events = {...BasicEvents, ...AdvEvents};

let e: Events = Events.Pause;

D'après mes tests, il semble que Events.Start soit correctement interprété comme BasicEvents.Start dans le système de type, donc la vérification de l'exhaustivité et le raffinement de l'union discriminée semblent bien fonctionner. La principale chose qui manque est que vous ne pouvez pas utiliser Events.Pause comme littéral de type ; vous avez besoin AdvEvents.Pause . Vous pouvez utiliser typeof Events.Pause et cela se résout en AdvEvents.Pause , bien que les membres de mon équipe aient été confus par ce type de modèle et je pense qu'en pratique, j'encouragerais AdvEvents.Pause lors de l'utilisation comme un type.

(C'est pour le cas où vous voulez que les types d'énumération soient assignables entre eux plutôt que des énumérations isolées. D'après mon expérience, il est plus courant de vouloir qu'ils soient assignables.)

Une autre suggestion (même si cela ne résout pas le problème d'origine), que diriez-vous d'utiliser des littéraux de chaîne pour créer une union de type à la place ?

type BEs = "Start" | "Finish";

type AEs = BEs | "Pause" | "Resume";

let example: AEs = "Finish"; // there is even autocompletion

Alors, la solution à nos problèmes pourrait être celle-ci ?

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};
// { Start: string, Finish: string };


const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
} as const
// { Start: 'Start', Finish: 'Finish' };

https://github.com/Microsoft/TypeScript/pull/29510

L'extension des énumérations devrait être une fonctionnalité essentielle de TypeScript. Je dis juste

@wottpal répétant ma question de tout à l'heure :

Si [les énumérations peuvent être étendues], dans quelle mesure cela correspond-il au fait que les types d'énumération sont censés être finaux et impossibles à étendre ?

Plus précisément, il me semble que la vérification de la totalité d'une instruction switch sur une valeur enum dépend de la non-extensibilité des enums.

@masak Quoi ? Non, ce n'est pas le cas ! Étant donné que l'énumération étendue est un type plus large et ne peut pas être affectée à l'énumération d'origine, vous connaissez toujours toutes les valeurs de chaque énumération que vous utilisez. Dans ce contexte, étendre signifie créer une nouvelle énumération, et non modifier l'ancienne.

enum A { a; }
enum B extends A { b; }

declare var a: A;
switch(a) {
    case A.a:
        break;
    default:
        // a is never
}

declare var b: B;
switch(b) {
    case A.a:
        break;
    default:
        // b is B.b
}

@ m93a Ah, donc vous voulez dire que extends ici a en fait plus d'une sémantique _copying_ (des valeurs enum de A dans B )? Alors, oui, les interrupteurs sortent OK.

Cependant, il y a _certaines_ attentes là-dedans qui me semblent toujours brisées. Pour essayer de comprendre : avec les classes, extends ne transmet _pas_ la sémantique de copie — les champs et les méthodes ne sont pas copiés dans la sous-classe d'extension ; au lieu de cela, ils sont simplement mis à disposition via la chaîne de prototypes. Il n'y a jamais qu'un seul champ ou méthode dans la superclasse.

Pour cette raison, si class B extends A , nous sommes assurés que B est assignable à A , et donc par exemple let a: A = new B(); serait parfaitement bien.

Mais avec les énumérations et extends , nous ne pourrions pas faire let a: A = B.b; , car il n'y a pas de garantie correspondante. C'est ce qui me semble étrange; extends ici transmet un certain ensemble d'hypothèses sur ce qui peut être fait, et elles ne sont pas remplies avec des énumérations.

Ensuite, appelez-le simplement expands ou clones ? 🤷‍♂️
Du point de vue des utilisateurs, il est tout simplement étrange que quelque chose d'aussi basique ne soit pas simple à réaliser.

Si la sémantique raisonnable nécessite un tout nouveau mot-clé (sans beaucoup d'art antérieur dans d'autres langages), pourquoi ne pas réutiliser à la place la syntaxe de propagation ( ... ) comme suggéré dans OP et ce commentaire ?

+1 J'espère que cela sera ajouté à la fonction d'énumération par défaut. :)

Est-ce que quelqu'un connaît des solutions élégantes ??? 🧐

Si la sémantique raisonnable nécessite un tout nouveau mot-clé (sans beaucoup d'art antérieur dans d'autres langages), pourquoi ne pas plutôt réutiliser la syntaxe de diffusion (...) comme suggéré dans OP et ce commentaire ?

Oui, après y avoir réfléchi un peu plus, je pense que cette solution serait bonne.

Après avoir lu tout ce fil de discussion, il semble qu'il y ait un large accord sur le fait que la réutilisation de l'opérateur de propagation résout le problème et répond à toutes les préoccupations que les gens ont soulevées concernant le fait de rendre la syntaxe confuse/non intuitive.

// extend enum using spread
enum AdvancedEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

Ce problème a-t-il vraiment encore besoin de l'étiquette "En attente de plus de commentaires" à ce stade, @RyanCavanaugh ?

La fonctionnalité recherchée +1

Avons-nous des nouvelles à ce sujet ? Il est vraiment utile d'avoir l'opérateur de diffusion implémenté pour les énumérations.

Surtout pour les cas d'utilisation qui impliquent la métaprogrammation, la possibilité d'aliaser et d'étendre les énumérations se situe quelque part entre un must-have et un nice-to-have. Il n'y a actuellement aucun moyen de prendre une énumération et export sous un autre nom - à moins que vous n'ayez recours à l'une des solutions de contournement mentionnées ci-dessus.

@ m93a Ah, donc vous voulez dire que extends ici a en fait plus d'une sémantique _copying_ (des valeurs enum de A dans B )? Alors, oui, les interrupteurs sortent OK.

Cependant, il y a _certaines_ attentes là-dedans qui me semblent toujours brisées. Pour essayer de comprendre : avec les classes, extends ne transmet _pas_ la sémantique de copie — les champs et les méthodes ne sont pas copiés dans la sous-classe d'extension ; au lieu de cela, ils sont simplement mis à disposition via la chaîne de prototypes. Il n'y a jamais qu'un seul champ ou méthode dans la superclasse.

Pour cette raison, si class B extends A , nous sommes assurés que B est assignable à A , et donc par exemple let a: A = new B(); serait parfaitement bien.

Mais avec les énumérations et extends , nous ne pourrions pas faire let a: A = B.b; , car il n'y a pas de garantie correspondante. C'est ce qui me semble étrange; extends ici transmet un certain ensemble d'hypothèses sur ce qui peut être fait, et elles ne sont pas remplies avec des énumérations.

@masak Je pense que vous êtes sur le point de corriger mais vous avez fait une petite hypothèse qui est incorrecte. B est assignable à A en cas de enum B extends A car "assignable" signifie que toutes les valeurs fournies par A sont disponibles dans B. Lorsque vous dites que let a: A = B.b vous supposez que les valeurs de B doivent être disponible dans A, ce qui n'est pas la même chose que les valeurs pouvant être attribuées à A. let a: A = B.a EST correct car B est attribuable à A.

Cela est évident en utilisant des classes comme dans l'exemple suivant :

class A {
 a() {}
}

class B extends A {
 b() {}
}

let a: A = new B();

a.a();  // valid
a.b();  // invalid via type system since `a` is typed as `A`

Lien de terrain de jeu TypeScript

invalid access

Pour faire court, je crois qu'il étend la terminologie correcte car c'est exactement ce qui est fait. Dans l'exemple enum B extends A , vous pouvez TOUJOURS vous attendre à ce qu'une énumération B contienne toutes les valeurs possibles de l'énumération A, car B est une "sous-classe" (sous-énumération ? Il existe peut-être un meilleur mot pour cela) de A et donc attribuable à A.

Donc je ne pense pas que nous ayons besoin d'un nouveau mot-clé, je pense que nous devrions utiliser des extensions ET je pense que cela devrait faire nativement partie de TypeScript :D

@julian-sf Je pense que je suis d'accord avec tout ce que vous avez écrit...

...mais... :visage_légèrement_souriant:

comme je l'ai problématisé ici , qu'en est-il de cette situation ?

// example from OP
enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

Étant donné que Pause est une _instance_ de AdvEvents et que AdvEvents _extends_ BasicEvents , vous attendriez-vous également à ce que Pause soit une _instance_ de BasicEvents ? (Parce que cela semble découler de la façon dont les relations instance/héritage interagissent habituellement.)

D'un autre côté, la proposition de valeur fondamentale des énumérations (IMHO) est qu'elles sont _fermées_/"finales" (comme dans, non extensibles) de sorte que quelque chose comme une déclaration switch peut assumer la totalité. (Et donc AdvEvents pouvoir _étendre_ ce que signifie être un BasicEvent viole une sorte de moindre surprise pour les énumérations.)

Je ne pense pas que vous puissiez plus de deux des trois propriétés suivantes :

  • Énumérations fermées/finales/totales prévisibles
  • Une relation extends entre deux déclarations enum
  • L'hypothèse (raisonnable) que si b est une instance de B et B extends A , alors b est une instance de A

@masak Je comprends et suis d'accord avec le principe fermé des énumérations (au moment de l'exécution). Mais l'extension du temps de compilation ne violerait pas le principe fermé au moment de l'exécution, car ils seraient tous définis et construits par le compilateur.

L'hypothèse (raisonnable) que si b est une instance de B et que B étend A, alors b est une instance de A

Je pense que ce raisonnement est un peu trompeur car la dichotomie instance/classe n'est pas vraiment attribuable à enum. Les énumérations ne sont pas des classes et elles n'ont pas d'instances. Je pense cependant qu'ils peuvent être extensibles, s'ils sont faits correctement. Pensez aux énumérations plus comme des ensembles. Dans cet exemple, B est un sur-ensemble de A. Par conséquent, il est raisonnable de supposer que toute valeur de A est présente dans B, mais que seules QUELQUES valeurs de B seront présentes dans A.

Je comprends d'où vient le souci... quoique. Et je ne sais pas quoi faire à ce sujet. Un bon exemple de problème avec l'extension enum :

const enum A { a = 'a' }
const enum B extends A { b = 'b' }

const foo = (a: A) => console.log(a);
const bar = (b: B) => foo(b);

bar(B.a); // 'a'
bar(B.b); // uh-oh, b doesn't exist on A, so foo would get unexpected behavior

// HOWEVER, this would work just fine...

const baz = (a: A) => bar(a);

baz(A.a); // 'a'
baz(B.a); // 'a'
baz(B.b); // compiler error as expected...

Dans ce cas, les énumérations se comportent assez différemment des classes. S'il s'agissait de classes, vous vous attendriez à pouvoir convertir B en A assez facilement, mais cela ne fonctionnera clairement pas ici. Je ne pense pas nécessairement que ce soit MAUVAIS, je pense que cela devrait simplement être pris en compte. IE, vous ne pouvez pas étendre un type enum vers le haut dans son arbre d'héritage comme une classe. Cela pourrait être expliqué par une erreur du compilateur du type "impossible d'affecter l'énumération du sur-ensemble B à A, car toutes les valeurs de B ne sont pas présentes dans A".

@julian-sf

Je pense que ce raisonnement est un peu trompeur car la dichotomie instance/classe n'est pas vraiment attribuable à enum. Les énumérations ne sont pas des classes et elles n'ont pas d'instances.

Vous avez tout à fait raison, à première vue.

  • Considérée comme une construction de langage indépendante, une énumération a des _membres_, pas des instances. Le terme « membre » est utilisé à la fois dans le manuel et dans la spécification linguistique. (C# et Python utilisent de la même manière le terme "membre". Java utilise "constante enum", car "membre" a une signification surchargée en Java.)
  • Du point de vue du code compilé, une énumération a des _properties_, mappant dans les deux sens - les noms aux valeurs et les valeurs aux noms. Encore une fois, pas d'instances.

En pensant à cela, je me rends compte que je suis un peu coloré par la vision de Java sur les énumérations. En Java, les valeurs enum sont littéralement des instances de leur type enum. Du point de vue de l'implémentation, une énumération est une classe étendant la classe Enum . (Vous n'êtes pas autorisé à le faire manuellement , vous devez passer par le mot-clé enum , mais c'est ce qui se passe sous le capot.) La bonne chose à ce sujet est que les énumérations obtiennent toutes les commodités des classes : elles peut avoir des champs, des constructeurs, des méthodes... Dans cette approche, les membres enum _sont_ des instances. (Le JLS en dit autant.)

Notez que je ne propose aucune modification de la sémantique des énumérations TypeScript. En particulier, je ne dis pas que TypeScript devrait passer à l'utilisation du modèle Java pour les énumérations. Je dis qu'il est instructif/intelligent de superposer une "compréhension" de classe/instance au-dessus des membres enums/enum. Pas "un enum _is_ a class" ou "un enum member _is_ a instance"... mais il y a des similitudes qui se perpétuent.

Quelles similitudes ? D'abord et avant tout, tapez l'adhésion.

enum Foo { A, B, C }
enum Bar { X, Y, Z }

let foo: Foo = Foo.C;
foo = Bar.Z;

La dernière ligne ne vérifie pas le type, car Bar.Z n'est pas un Foo . Encore une fois, ce ne sont pas _réellement_ des classes et des instances, mais cela peut être _compris_ en utilisant le même modèle, comme si Foo et Bar étaient des classes et que les six membres étaient leurs instances respectives.

(Nous ignorerons pour les besoins de cet argument le fait que let foo: Foo = 2; est également sémantiquement légal, et qu'en général, les valeurs number sont assignables aux variables de type enum.)

Les énumérations ont la propriété supplémentaire qu'elles sont _fermées_ — désolé, je ne connais pas de meilleur terme pour cela — une fois que vous les avez définies, vous ne pouvez pas les étendre. Plus précisément, les membres répertoriés à l'intérieur de la déclaration enum sont les _seuls_ éléments dont le type correspond au type enum. ("Fermé" comme dans "hypothèse du monde fermé".) C'est une excellente propriété car vous pouvez vérifier avec une certitude totale que tous les cas d'une instruction switch sur votre énumération ont été couverts.

Avec extends sur les énumérations, cette propriété sort de la fenêtre.

Vous écrivez,

Je comprends et suis d'accord avec le principal fermé des énumérations (à l'exécution). Mais l'extension du temps de compilation ne violerait pas le principe fermé au moment de l'exécution, car ils seraient tous définis et construits par le compilateur.

Je ne pense pas que ce soit vrai, car cela suppose que tout code qui étend votre énumération se trouve dans votre projet. Mais un module tiers peut étendre votre enum, et soudainement il y a des membres _new_ enum qui sont également assignables à votre enum, en dehors du contrôle du code que vous compilez. Essentiellement, les énumérations ne seraient plus fermées, même au moment de la compilation.

J'ai toujours l'impression d'être un peu maladroit pour exprimer exactement ce que je veux dire, mais je pense que c'est important : extends sur enum casserait l'une des caractéristiques les plus précieuses des énumérations, le fait qu'elles ' re fermé. Veuillez compter le nombre de langues absolument _interdites_ à étendre/sous-classer une énumération, pour cette raison même.

J'ai pensé un peu plus aux solutions de contournement, et en travaillant à partir de # 17592 (commentaire) , vous pouvez faire un peu mieux en définissant également Events dans l'espace de valeur :

enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;
const Events = {...BasicEvents, ...AdvEvents};

let e: Events = Events.Pause;

D'après mes tests, il semble que Events.Start soit correctement interprété comme BasicEvents.Start dans le système de type, donc la vérification de l'exhaustivité et le raffinement de l'union discriminée semblent bien fonctionner. La principale chose qui manque est que vous ne pouvez pas utiliser Events.Pause comme littéral de type ; vous avez besoin AdvEvents.Pause . Vous _pouvez_ utiliser typeof Events.Pause et cela se résout en AdvEvents.Pause , bien que les membres de mon équipe aient été troublés par ce type de modèle et je pense qu'en pratique, j'encouragerais AdvEvents.Pause lors de l'utilisation comme un type.

(C'est pour le cas où vous voulez que les types d'énumération soient assignables entre eux plutôt que des énumérations isolées. D'après mon expérience, il est plus courant de vouloir qu'ils soient assignables.)

Je pense que c'est la meilleure solution à portée de main, en ce moment.

Merci @alangpierce :+1:

Une mise à jour pour ceci?

@sdwvit Je ne fais pas partie des personnes principales, mais de mon point de vue, la proposition de syntaxe suivante (de OP, mais re-suggérée deux fois par la suite) rendrait tout le monde heureux, sans aucun problème connu :

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

Cela me rendrait _me_ heureux, car cela signifierait implémenter cette fonctionnalité apparemment utile "dupliquer tous les membres de cet autre enum" _sans_ utiliser extends , ce que je considère comme problématique pour les raisons que j'ai indiquées. La syntaxe ... évite ces problèmes en copiant et non en étendant.

Le problème est toujours marqué comme "En attente de plus de commentaires", et je respecte le droit des membres principaux de le conserver dans cette catégorie aussi longtemps qu'ils le jugent nécessaire. Mais aussi, rien n'empêche quiconque de mettre en œuvre ce qui précède et de le soumettre en tant que PR.

@masak merci pour la réponse. Je dois maintenant parcourir tout l'historique de la discussion. Je vous répondrai après :)

J'aimerais absolument que cela se produise et j'aimerais absolument essayer de le mettre en œuvre moi-même. Cependant, nous devons encore définir des comportements pour toutes les énumérations. Tout cela fonctionne bien pour les énumérations basées sur des chaînes, mais qu'en est-il des énumérations numériques vanille. Comment fonctionne l'extension/la copie ici ?

  • Je suppose que nous ne voudrons autoriser l'extension d'une énumération qu'avec une énumération "du même type" (numérique étend numérique, chaîne étend chaîne). Les énumérations hétérogènes sont techniquement prises en charge, donc je suppose que nous devrions conserver cette prise en charge.

  • Devrions-nous autoriser l'extension à partir de plusieurs énumérations ? Devraient-ils tous avoir des valeurs mutuellement exclusives ? Ou allons-nous autoriser les valeurs qui se chevauchent ? Priorité basée sur l'ordre lexical ?

  • Les énumérations étendues peuvent-elles remplacer les valeurs des énumérations étendues ?

  • Les énumérations étendues doivent-elles apparaître au début de la liste de valeurs ou peuvent-elles être dans n'importe quel ordre ? Je suppose que les valeurs définies ultérieurement ont une priorité plus élevée ?

  • Je suppose que les valeurs numériques implicites continueront 1 après la valeur maximale des énumérations numériques étendues.

  • Considérations spéciales pour les masques de bits ?

etc.

@JeffreyMercado Ce sont de bonnes questions, et appropriées pour celui qui espère tenter une mise en œuvre. :le sourire:

Vous trouverez ci-dessous mes réponses, guidées par une approche de conception "conservatrice" (comme dans "prenons des décisions de conception qui interdisent les cas dont nous ne sommes pas sûrs, plutôt que de faire maintenant des choix difficiles à changer plus tard tout en restant rétrocompatible").

  • Je suppose que nous ne voudrons autoriser l'extension d'une énumération qu'avec une énumération "du même type" (numérique étend numérique, chaîne étend chaîne)

Je suppose aussi. L'énumération résultante de type mixte ne semble pas très utile.

  • Devrions-nous autoriser l'extension à partir de plusieurs énumérations ? Devraient-ils tous avoir des valeurs mutuellement exclusives ? Ou allons-nous autoriser les valeurs qui se chevauchent ? Priorité basée sur l'ordre lexical ?

Puisqu'il s'agit de copier la sémantique dont nous parlons, la duplication de plusieurs énumérations semble "plus acceptable" que l'héritage multiple à la C++. Je n'y vois pas immédiatement de problème, surtout si nous continuons à construire sur l'analogie de la propagation d'objets : let newEnum = { ...enumA, ...enumB };

Tous les membres devraient-ils avoir des valeurs mutuellement exclusives ? La chose conservatrice serait de dire "oui". Encore une fois, l'analogie de la propagation d'objets nous fournit une sémantique alternative : le dernier gagne.

Je ne vois aucun cas d'utilisation où j'apprécierais de pouvoir remplacer les valeurs enum. Mais c'est peut-être juste un manque d'imagination de ma part. L'approche conservatrice consistant à interdire les collisions a les propriétés agréables qu'il est facile d'expliquer/d'intérioriser, et au moins en théorie, elle pourrait exposer de véritables erreurs de conception (dans le code frais ou le code qui est maintenu).

  • Les énumérations étendues peuvent-elles remplacer les valeurs des énumérations étendues ?

Je pense que la réponse et le raisonnement sont sensiblement les mêmes dans ce cas que dans le cas précédent.

  • Les énumérations étendues doivent-elles apparaître au début de la liste de valeurs ou peuvent-elles être dans n'importe quel ordre ? Je suppose que les valeurs définies ultérieurement ont une priorité plus élevée ?

J'allais dire d'abord que cela n'a d'importance que si nous suivons la sémantique "le dernier gagne" de l'écrasement.

Mais à la réflexion, à la fois sous "pas de collisions" et "le dernier gagne", je trouve même bizarre de _vouloir_ mettre des déclarations de membres enum avant que les enum ne se propagent dans la liste. Par exemple, quelle intention est communiquée en faisant cela ? Les spreads sont un peu comme des "imports", et ceux-ci vont classiquement en haut.

Je ne veux pas nécessairement interdire de mettre des spreads enum après les déclarations de membres enum (bien que je pense que je serais d'accord pour que cela soit interdit dans la grammaire). Si cela finit par être autorisé, c'est certainement quelque chose que les linters et la convention communautaire pourraient signaler comme évitable. Il n'y a simplement aucune raison impérieuse de le faire.

  • Je suppose que les valeurs numériques implicites continueront 1 après la valeur maximale des énumérations numériques étendues.

Peut-être que la chose conservatrice à faire est d'exiger une valeur explicite pour le premier membre après une propagation.

  • Considérations spéciales pour les masques de bits ?

Je pense que cela serait couvert par la règle ci-dessus.

J'ai pu faire quelque chose de raisonnable en combinant des énumérations, des interfaces et des objets immuables.

export enum Unit {
    SECONDS,
    MINUTES,
    HOURS,
    DAYS,
    WEEKS,
    MONTHS,
    YEARS,
    DECADES,
    CENTURIES,
    MILLENNIA
}

interface Labels {
    SINGULAR: Record<Unit, string>
    PLURAL: Record<Unit, string>
    LAST: string;
    DELIM: string;
    NOW: string;
}

export const EnglishLabels: Labels = {
    SINGULAR: {
        [Unit.SECONDS]: ' second',
        [Unit.MINUTES]: ' minute',
        [Unit.HOURS]: ' hour',
        [Unit.DAYS]: ' day',
        [Unit.WEEKS]: ' week',
        [Unit.MONTHS]: ' month',
        [Unit.YEARS]: ' year',
        [Unit.DECADES]: ' decade',
        [Unit.CENTURIES]: ' century',
        [Unit.MILLENNIA]: ' millennium'
    },
    PLURAL: {
        [Unit.SECONDS]: ' seconds',
        [Unit.MINUTES]: ' minutes',
        [Unit.HOURS]: ' hours',
        [Unit.DAYS]: ' days',
        [Unit.WEEKS]: ' weeks',
        [Unit.MONTHS]: ' months',
        [Unit.YEARS]: ' years',
        [Unit.DECADES]: ' decades',
        [Unit.CENTURIES]: ' centuries',
        [Unit.MILLENNIA]: ' millennia'
    },
    LAST: ' and ',
    DELIM: ', ',
    NOW: ''
}

@illeatmyhat C'est une belle utilisation des énumérations, mais ... je ne vois pas en quoi cela compte comme l'extension d'une énumération existante. Ce que vous faites, c'est _utiliser_ l'énumération.

(De plus, contrairement aux enums et aux instructions switch, il semble que dans votre exemple vous n'ayez pas de vérification de la totalité ; quelqu'un qui a ajouté un membre enum plus tard pourrait facilement oublier d'ajouter une clé correspondante dans les SINGULAR et PLURAL record dans toutes les instances de Label .)

@masak

quelqu'un qui a ajouté un membre enum plus tard pourrait facilement oublier d'ajouter une clé correspondante dans les enregistrements SINGULAR et PLURAL dans toutes les instances de Label .)

Au moins dans mon environnement, il génère une erreur lorsqu'un membre enum est manquant dans SINGULAR ou PLURAL . Le type Record fait son travail, je suppose.

Bien que la documentation de TS soit bonne, je pense qu'il n'y a pas beaucoup d'exemples sur la façon de combiner de nombreuses fonctionnalités de manière non triviale. enum inheritance a été la première chose que j'ai recherchée lorsque j'ai essayé de résoudre des problèmes d'internationalisation, ce qui a conduit à ce fil. L'approche s'est avérée fausse de toute façon, c'est pourquoi j'ai écrit ce post.

@illeatmyhat

Au moins dans mon environnement, il génère une erreur lorsqu'un membre enum est manquant dans SINGULAR ou PLURAL . Le type Record fait son travail, je suppose.

Oh! TIL. Et oui, cela le rend beaucoup plus intéressant. Je vois ce que vous voulez dire par atteindre initialement l'héritage enum et finalement atterrir sur votre modèle. Ce n'est peut-être même pas une chose isolée; Les "problèmes X/Y" sont une réalité. Plus de gens pourraient commencer par penser "Je veux étendre MyEnum ", mais finir par utiliser Record<MyEnum, string> comme vous l'avez fait.

Répondre à @masak :

Avec des extensions sur les énumérations, cette propriété sort de la fenêtre.

Vous écrivez,

@julian-sf : Je comprends et suis d'accord avec le principe fermé des énumérations (au moment de l'exécution). Mais l'extension du temps de compilation ne violerait pas le principe fermé au moment de l'exécution, car ils seraient tous définis et construits par le compilateur.

Je ne pense pas que ce soit vrai, car cela suppose que tout code qui étend votre énumération se trouve dans votre projet. Mais un module tiers peut étendre votre enum, et soudainement il y a de nouveaux membres enum qui sont également assignables à votre enum, en dehors du contrôle du code que vous compilez. Essentiellement, les énumérations ne seraient plus fermées, même au moment de la compilation.

Plus j'y pense, plus vous avez raison. Les énumérations doivent être fermées. J'aime beaucoup l'idée de "composer" des énumérations car je pense que c'est vraiment le coeur du sujet que nous voulons ici 🥳.

Je pense que cette notation résume assez élégamment le concept de "collage" de deux énumérations distinctes :

enum ComposedEnum = { ...EnumA, ...EnumB }

Alors considérez que ma démission en utilisant le terme extends 😆


Commentaires sur les réponses de @masak aux questions de @JeffreyMercado :

  • Je suppose que nous ne voudrons autoriser l'extension d'une énumération qu'avec une énumération "du même type" (numérique étend numérique, chaîne étend chaîne). Les énumérations hétérogènes sont techniquement prises en charge, donc je suppose que nous devrions conserver cette prise en charge.

Je suppose aussi. L'énumération résultante de type mixte ne semble pas très utile.

Bien que je convienne que ce n'est pas utile, nous DEVONS probablement conserver ici un support hétérogène pour les énumérations. Je pense qu'un avertissement linter serait utile ici, mais je ne pense pas que TS devrait s'y opposer. Je peux penser à un cas d'utilisation artificiel qui est, je construis une énumération pour les interactions avec une API très mal conçue qui prend des drapeaux qui sont un mélange de nombres et de chaînes. Artificiel, je sais, mais puisque c'est autorisé ailleurs, je ne pense pas que nous devrions l'interdire ici.

Peut-être juste un fort encouragement à ne pas le faire ?

  • Devrions-nous autoriser l'extension à partir de plusieurs énumérations ? Devraient-ils tous avoir des valeurs mutuellement exclusives ? Ou allons-nous autoriser les valeurs qui se chevauchent ? Priorité basée sur l'ordre lexical ?

Puisqu'il s'agit de copier la sémantique dont nous parlons, la duplication de plusieurs énumérations semble "plus acceptable" que l'héritage multiple à la C++. Je n'y vois pas immédiatement de problème, surtout si nous continuons à construire sur l'analogie de la propagation d'objets : let newEnum = { ...enumA, ...enumB };

100 % d'accord

  • Tous les membres devraient-ils avoir des valeurs mutuellement exclusives ?

La chose conservatrice serait de dire "oui". Encore une fois, l'analogie de la propagation d'objets nous fournit une sémantique alternative : le dernier gagne.

Je suis déchiré ici. Bien que je sois d'accord pour dire que c'est la "meilleure pratique" d'imposer l'exclusivité mutuelle des valeurs, est-ce correct ? Il est directement contradictoire avec la sémantique de propagation communément connue. D'une part, j'aime l'idée d'imposer des valeurs mutuellement exclusives, d'autre part, cela brise beaucoup d'hypothèses sur la façon dont la sémantique de diffusion devrait fonctionner. Y a-t-il des inconvénients à suivre les règles de répartition normales avec "le dernier gagne" ? Il semble que ce soit plus facile à mettre en œuvre (car l'objet sous-jacent n'est de toute façon qu'une carte). Mais cela semble également correspondre aux attentes communes. Je penche pour être moins surprenant.

Il peut également y avoir de bons exemples pour vouloir remplacer une valeur (bien que je n'aie aucune idée de ce que cela serait).

Mais à la réflexion, à la fois sous "pas de collisions" et "le dernier gagne", je trouve bizarre de vouloir même mettre des déclarations de membres enum avant que les enum ne se propagent dans la liste. Par exemple, quelle intention est communiquée en faisant cela ? Les spreads sont un peu comme des "imports", et ceux-ci vont classiquement en haut.

Eh bien, cela dépend, si nous suivons la sémantique de propagation, alors l'ordre ne devrait pas avoir d'importance. Honnêtement, même si nous appliquons des valeurs mutuellement exclusives, l'ordre n'aurait pas vraiment d'importance, n'est-ce pas ? Une collision serait une erreur à ce stade, quel que soit l'ordre.

  • Je suppose que les valeurs numériques implicites continueront 1 après la valeur maximale des énumérations numériques étendues.

Peut-être que la chose conservatrice à faire est d'exiger une valeur explicite pour le premier membre après une propagation.

Je suis d'accord. Si vous diffusez une énumération, TS doit simplement appliquer des valeurs explicites pour les membres supplémentaires.

@julian-sf

Alors considérez que ma démission sur l'utilisation du terme se prolonge 😆

:+1: La Société pour la préservation des types de somme applaudit depuis la ligne de touche.

Mais à la réflexion, à la fois sous "pas de collisions" et "le dernier gagne", je trouve bizarre de vouloir même mettre des déclarations de membres enum avant que les enum ne se propagent dans la liste. Par exemple, quelle intention est communiquée en faisant cela ? Les spreads sont un peu comme des "imports", et ceux-ci vont classiquement en haut.

Eh bien, cela dépend, si nous suivons la sémantique de propagation, alors l'ordre ne devrait pas avoir d'importance. Honnêtement, même si nous appliquons des valeurs mutuellement exclusives, l'ordre n'aurait pas vraiment d'importance, n'est-ce pas ? Une collision serait une erreur à ce stade, quel que soit l'ordre.

Je dis "il n'y a aucune bonne raison de placer des spreads après les déclarations normales des membres" ; vous dites "sous les restrictions appropriées, les placer avant ou après ne fait aucune différence". Ces deux choses peuvent être vraies en même temps.

La principale différence de résultat semble tomber sur un spectre d'autorisation ou d'interdiction des spreads avant les membres normaux. Cela pourrait être syntaxiquement interdit; cela pourrait produire un avertissement de charpie; ou il pourrait être tout à fait bien à tous égards. Si l'ordre ne fait aucune différence sémantique, il s'agit alors de faire en sorte que la fonction de diffusion enum suive le principe de la moindre surprise , facile à utiliser et facile à enseigner/expliquer.

L'utilisation de l'opérateur de propagation s'inscrit dans l'utilisation plus large de la copie superficielle dans JS et TypeScript. C'est certainement la méthode la plus largement utilisée et la plus facile à comprendre que l'utilisation de extends , ce qui implique une relation directe. Créer une énumération par composition serait la solution la plus facile à consommer.

Certaines des suggestions de contournement, bien que valides et utilisables, ajoutent beaucoup plus de code passe-partout pour obtenir le même résultat souhaité. Étant donné la nature finale et immuable d'une énumération, il serait souhaitable de créer des énumérations supplémentaires par composition, afin de conserver les propriétés cohérentes avec les autres langages.

C'est juste dommage que 3 ans sur cette conversation dure encore.

@jmitchell38488 Je laisserais un like à votre commentaire, mais votre dernière phrase m'a fait changer d'avis. Il s'agit d'une discussion nécessaire, car la solution proposée fonctionnerait, mais elle implique également la possibilité d'étendre les classes et les interfaces de cette façon. C'est un grand changement qui peut effrayer certains programmeurs de langages de type C++ d'utiliser du script dactylographié, puisque vous vous retrouvez essentiellement avec 2 façons de faire la même chose ( class A extends B et class A { ...(class B {}) } ). Je pense que les deux méthodes peuvent être prises en charge, mais nous avons également besoin extend pour les énumérations pour des raisons de cohérence.

@masak wdyt? ^

@sdwvit Je ne parle pas de changer le comportement pour créer des classes et des interfaces, je parle spécifiquement des énumérations et de les créer par composition. Ce sont des types finaux immuables, nous ne devrions donc pas pouvoir nous étendre de la manière typique de l'héritage.

Compte tenu de la nature de JS et de la valeur transpilée finale, il n'y a aucune raison pour que la composition ne puisse pas être réalisée. Bien sûr, cela rendrait le travail avec les énumérations plus attrayant.

@masak wdyt? ^

Hum. Je pense que la cohérence du langage est un objectif louable, et donc ce n'est pas _a priori_ faux de demander une fonctionnalité ... similaire dans les classes et les interfaces. Mais je pense que le cas est plus faible ou inexistant là-bas, et pour deux raisons : (a) les classes et les interfaces ont déjà un mécanisme d'extension, et en ajouter un second apporte peu de valeur supplémentaire (alors qu'en fournir un pour les énumérations couvrirait un cas d'utilisation que les gens reviennent sur cette question depuis des années); (b) l'ajout d'une nouvelle syntaxe et sémantique aux classes a une barre d'approbation beaucoup plus élevée, puisque les classes sont en quelque sorte issues de la spécification EcmaScript. (TypeScript est plus ancien que ES6, mais il y a eu un effort actif pour que le premier reste proche du second. Cela inclut de ne pas introduire de fonctionnalités supplémentaires en plus.)

Je pense que ce fil est ouvert depuis longtemps simplement parce qu'il représente une fonctionnalité intéressante qui couvrirait des cas d'utilisation réels, mais il n'a pas encore fait l'objet d'un PR. Faire un tel PR prend du temps et des efforts, plus que de simplement dire que vous voulez la fonctionnalité. :clin d'œil:

Est-ce que quelqu'un travaille sur cette fonctionnalité ?

Est-ce que quelqu'un travaille sur cette fonctionnalité ?

Je suppose que non, puisque nous n'avons même pas terminé la discussion à ce sujet, haha !

J'ai l'impression que nous nous sommes rapprochés d'un consensus sur ce à quoi cela ressemblerait. Puisqu'il s'agirait d'un ajout de langage, il faudrait probablement un « champion » pour faire avancer cette proposition. Je pense que quelqu'un de l'équipe TS devrait venir et déplacer le problème de "En attente de commentaires" à "En attente de proposition" (ou quelque chose de similaire).

Je travaille sur un prototype de celui-ci. Bien que je ne sois pas allé très loin en raison du manque de temps et de la méconnaissance de la structure du code. Je veux aller jusqu'au bout et s'il n'y a pas d'autre mouvement, je continuerai quand je le pourrai.

Aimerait aussi cette fonctionnalité. 37 mois se sont écoulés, espérons que des progrès seront bientôt réalisés

Notes de la dernière réunion :

  • Nous aimons la syntaxe de propagation, car extends implique un sous-type, alors que "l'extension" d'une énumération crée un supertype.

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     ...BasicEvents,
     Pause = "Pause",
     Resume = "Resume"
    }
    
  • L'idée est que AdvEvents.Start résoudrait la même identité de type que BasicEvents.Start . Cela implique que les types BasicEvents.Start et AdvEvents.Start seraient assignables l'un à l'autre, et le type BasicEvents serait assignable à AdvEvents . J'espère que cela a un sens intuitif, mais il est important de noter que cela signifie que la propagation n'est pas simplement un raccourci syntaxique - si nous étendons la propagation à ce qu'elle ressemble, cela signifie :

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     Start = "Start",
     Finish = "Finish",
     Pause = "Pause",
     Resume = "Resume"
    }
    

    cela a un comportement différent - ici, BasicEvents.Start et AdvEvents.Start ne sont _pas_ assignables l'un à l'autre, en raison de la qualité opaque des énumérations de chaînes.

    Une autre conséquence mineure de cette implémentation est que AdvEvents.Start serait probablement sérialisé comme BasicEvents.Start dans les informations rapides et l'émission de déclaration (au moins là où il n'y a pas de contexte syntaxique pour lier le membre à AdvEvents — passer la souris sur l'expression littérale AdvEvents.Start pourrait vraisemblablement donner des informations rapides indiquant AdvEvents.Start , mais il serait peut-être plus clair d'afficher néanmoins BasicEvents.Start ).

  • Cela ne serait autorisé que dans les énumérations de chaînes.

J'aimerais essayer celui-ci.

Pour clarifier: Ceci n'est pas approuvé pour la mise en œuvre.

Il y a deux comportements possibles ici, et ils semblent tous les deux mauvais.

Option 1 : C'est en fait du sucre

Si spread signifie vraiment la même chose que copier les membres de l'énumération propagée, alors il y aura une grande surprise lorsque les gens essaieront d'utiliser la valeur de l'énumération étendue comme s'il s'agissait de la valeur de l'énumération étendue et cela ne fonctionne pas.

Option 2 : ce n'est pas vraiment du sucre

L'option préférée serait que spread fonctionne plus comme la suggestion de type union de @ aj-r près du haut du fil. Si c'est le comportement que les gens veulent déjà, alors les options existantes sur la table semblent strictement préférables pour des raisons de compréhension. Sinon, nous créons un nouveau type d'énumération de chaîne qui ne se comporte pas comme n'importe quelle autre énumération de chaîne, ce qui est bizarre et semble saper la "simplicité" de la suggestion ici.

Quel comportement les gens souhaitent-ils, et pourquoi ?

Je ne veux pas de l'option 1, car elle mène à de grosses surprises.

Je veux l'option 2, mais j'aimerais avoir suffisamment de support de syntaxe pour surmonter l'inconvénient @aj-r mentionné, afin que je puisse écrire let e: Events = Events.Pause; partir de son exemple. L'inconvénient n'est pas terrible, mais c'est un endroit où l'énumération étendue ne peut pas cacher l'implémentation ; donc c'est un peu dégoûtant.

Je pense aussi que l'option 1 est une mauvaise idée. J'ai recherché des références à ce problème dans mon entreprise et j'ai trouvé deux revues de code où il était lié, et dans les deux cas (et dans mon expérience personnelle), il est clairement nécessaire que les éléments de l'énumération plus petite soient attribuables au plus grand type d'énumération . Je m'inquiète particulièrement du fait qu'une personne présente ... pensant qu'elle se comporte comme l'option 2, puis que la personne suivante devienne vraiment confuse (ou recoure à des hacks comme as unknown as Events.Pause ) lorsque des cas plus complexes ne fonctionnent pas.

Il existe déjà de nombreuses façons d' essayer d'obtenir le comportement de l'option 2 : de nombreux extraits de code dans ce fil, ainsi que diverses approches impliquant des unions littérales de chaîne. Mon gros souci avec la mise en œuvre de l'option 1 est qu'elle introduit effectivement une autre mauvaise façon d'obtenir l'option 2, et conduit ainsi à plus de dépannage et de frustration pour les personnes qui apprennent cette partie de TypeScript.

Quel comportement les gens souhaitent-ils, et pourquoi ?

Puisque le code parle plus fort que les mots, et en utilisant l'exemple de l'OP :

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause", // We added a new field
    Finish = "Finish2" // Oops, we actually modified a field in the parent enum
};
// The TypeScript compiler should refuse to compile this code
// But after removing the "Finish2" line,
// the TypeScript compiler should successfully handle it as one would normally expect with the spread operator

Cet exemple présente l'option 2, n'est-ce pas ? Si oui, alors je veux désespérément l'option 2.

Sinon, nous créons un nouveau type d'énumération de chaîne qui ne se comporte pas comme n'importe quelle autre énumération de chaîne, ce qui est bizarre et semble saper la "simplicité" de la suggestion ici

Je conviens que l'option 2 est un peu troublante et qu'il vaut peut-être mieux prendre du recul et réfléchir à des alternatives. Voici une exploration de la façon dont cela pourrait être fait sans ajouter de nouvelle syntaxe ou de comportement aux énumérations d'aujourd'hui :

Je pense que ma suggestion dans https://github.com/microsoft/TypeScript/issues/17592#issuecomment -449440944 se rapproche le plus ces jours-ci et pourrait être quelque chose à travailler:

type Events = BasicEvents | AdvEvents;
const Events = {...BasicEvents, ...AdvEvents};

Je vois deux problèmes principaux avec cette approche :

  • C'est vraiment déroutant pour quelqu'un qui apprend TypeScript; si vous ne maîtrisez pas bien l'espace de type par rapport à l'espace de valeur, il semble qu'il écrase un type avec une constante.
  • Il est incomplet car il ne vous permet pas d'utiliser Events.Pause comme type.

J'ai précédemment suggéré https://github.com/microsoft/TypeScript/issues/29130 qui traiterait (entre autres) le deuxième point, et je pense que cela pourrait toujours être utile, même si cela ajouterait certainement beaucoup de subtilité à comment fonctionnent les noms.

Une idée de syntaxe qui, je pense, traiterait les deux points est une syntaxe alternative const qui déclare également un type :

// Declares a value and a type at the same time with the same name (just like `enum` and `class` already do).
// Requires the right-hand side to be either a unit type or an object where all values are unit types.
// The JS emit would just take out the word "type" and leave everything else.
const type Events = {...BasicEvents, ...AdvEvents};
...
const e: Events.Pause = Events.Pause;
...
// The syntax could also make this pattern more ergonomic.
const type INACCESSIBLE = "INACCESSIBLE";
const response: {name: string, favoriteColor: string} | INACCESSIBLE = INACCESSIBLE;

Cela se rapprocherait d'un monde où les énumérations ne sont pas du tout nécessaires. (Pour moi, les énumérations ont toujours eu l'impression d'aller à l'encontre des objectifs de conception TS car il s'agit d'une syntaxe au niveau de l'expression avec un comportement d'émission non trivial et nominal par défaut.) La syntaxe de déclaration const type vous permettrait également créez une énumération de chaîne structurellement typée comme ceci :

const type BasicEvents = {
  Start: 'Start',
  Finish: 'Finish',
};  // "as const" would be implicit for "const type" declarations.

Certes, la façon dont cela devrait fonctionner est que le type BasicEvents est un raccourci pour typeof BasicEvents[keyof typeof BasicEvents] , ce qui pourrait être trop ciblé pour une syntaxe au nom générique comme const type . Peut-être qu'un autre mot-clé serait mieux. Dommage const enum soit déjà pris 😛.

À partir de là, je pense que le seul écart entre les énumérations (chaînes) et les littéraux d'objet serait le typage nominal. Cela pourrait éventuellement être résolu en utilisant une syntaxe comme as unique ou const unique type qui opte essentiellement pour un comportement de typage nominal de type enum pour ces types d'objets, et idéalement pour les constantes de chaîne régulières également. Cela donnerait également un choix clair entre l'option 1 et l'option 2 lors de la définition Events : vous utilisez unique lorsque vous souhaitez que Events soit un type complètement distinct (option 1), et vous omettez unique lorsque vous voulez l'assignabilité entre Events et BasicEvents (option 2).

Avec const type et const unique type , il y aurait un moyen d'unir proprement les énumérations existantes, et aussi un moyen d'exprimer les énumérations comme une combinaison naturelle de fonctionnalités TS plutôt qu'une seule.

Qu'est-ce qu'il se passe ici? 😅

wow à partir de 2017 🤪, de quoi avez-vous besoin de plus de commentaires ?

de quoi avez-vous besoin de plus ?

Ici??? Nous ne nous contentons pas de mettre en œuvre des suggestions parce qu'elles sont anciennes !

de quoi avez-vous besoin de plus ?

Ici??? Nous ne nous contentons pas de mettre en œuvre des suggestions parce qu'elles sont anciennes !

Oui. Je n'étais pas sûr de quelle manière c'était.

En relisant et en voyant aussi # 40998, je pense que c'est la meilleure façon ... les emuns sont des objets et je pense que la propagation est plus facile pour fusionner/étendre les énumérations.

Je ne pense pas être suffisamment qualifié pour donner mon avis sur la conception du langage, mais je pense pouvoir donner mon avis en tant que développeur régulier.

J'ai rencontré ce problème quelques semaines plus tôt dans un projet réel où je voulais utiliser cette fonctionnalité. J'ai fini par utiliser l' approche de @alangpierce . Malheureusement, en raison de mes responsabilités envers mon employeur, je ne peux pas partager le code ici, mais voici quelques points :

  1. La répétition des déclarations (type et const pour une nouvelle énumération) n'était pas un gros problème et n'endommageait pas beaucoup la lisibilité.
  2. Dans mon cas, enum représentait différentes actions pour un certain algorithme, et il y avait différents ensembles d'actions pour différentes situations dans cet algorithme. L'utilisation de la hiérarchie des types m'a permis de vérifier que certaines actions ne pouvaient pas se produire au moment de la compilation : c'était le but de tout cela et s'est avéré très utile.
  3. Le code que j'ai écrit était une bibliothèque interne, et même si la distinction entre les différents types d'énumération était importante à l'intérieur de la bibliothèque, cela n'avait pas d'importance pour les utilisateurs extérieurs de cette bibliothèque. En utilisant cette approche, j'ai pu exporter un seul type qui était le type de somme de toutes les différentes énumérations à l'intérieur et masquer les détails de l'implémentation.
  4. Malheureusement, je n'ai pas été en mesure de trouver un moyen idiomatique et facile à lire pour analyser automatiquement les valeurs du type somme à partir des déclarations de type. (Dans mon cas, différentes étapes d'algorithme que j'ai mentionnées ont été chargées à partir de la base de données SQL lors de l'exécution). Ce n'était pas un gros problème (écrire le code d'analyse manuellement était assez simple), mais ce serait bien si l'implémentation de l'héritage enum accordait une certaine attention à ce problème.

Dans l'ensemble, je pense que beaucoup de projets réels avec une logique métier ennuyeuse bénéficieraient beaucoup de cette fonctionnalité. Diviser les énumérations en différents sous-types permettrait au système de type de vérifier de nombreux invariants qui sont maintenant vérifiés par les tests unitaires, et rendre les valeurs incorrectes non représentables par un système de type est toujours une bonne chose.

Salut,

Permettez-moi d'ajouter mes deux cents ici 🙂

Mon contexte

J'ai une API, avec la documentation OpenApi générée avec tsoa .

Un de mes modèles a un statut défini comme ceci :

enum EntityStatus {
    created = 'created',
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
    archived = 'archived',
}

J'ai une méthode setStatus qui prend un sous-ensemble de ces statuts. Étant donné que la fonctionnalité n'est pas disponible, j'ai envisagé de dupliquer l'énumération de cette façon :

enum RequestedEntityStatus {
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
}

Donc, ma méthode est décrite de cette façon:

public setStatus(status: RequestedEntityStatus) {
   this.status = status;
}

avec ce code, j'obtiens cette erreur :

Conversion of type 'RequestedEntityStatus' to type 'EntityStatus' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

ce que je ferai pour l'instant, mais j'étais curieux et j'ai commencé à chercher dans ce référentiel, quand j'ai trouvé ceci.
Après avoir fait défiler tout le chemin vers le bas, je n'ai trouvé personne (ou peut-être que j'ai raté quelque chose) qui suggère ce cas d'utilisation.

Dans mon cas d'utilisation, je ne veux pas "étendre" à partir d'une énumération car il n'y a aucune raison pour qu'EntityStatus soit une extension de RequestedEntityStatus. Je préférerais pouvoir "Choisir" dans l'énumération plus générique.

Ma proposition

J'ai trouvé l'opérateur de propagation meilleur que la proposition d'extension, mais j'aimerais aller plus loin et suggérer ceci :

enum EntityStatus {
    created = 'created',
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
    archived = 'archived',
}

enum RequestedEntityStatus {
    // Pick/Reuse from EntityStatus
    EntityStatus.started,
    EntityStatus.paused,
    EntityStatus.stopped,
}

// Fake enum, just to demonstrate
enum TargetStatus {
    {...RequestedEntityStatus},
    // Why not another spread here?
    //{...AnotherEnum},
    EntityStatus.archived,
}

public class Entity {
    private status: EntityStatus = 'created'; // Why not a cast here, if types are compatible, and deserializable from a JSON. EntityStatus would just act as a type union here.

    public setStatus(requestedStatus: RequestedEntityStatus) {
        if (this.status === (requestedStatus as EntityStatus)) { // Should be OK because types are compatible, but the cast would be needed to avoid comparing oranges and apples
            return;
        }

        if (requestedStatus == RequestedStatus.stopped) { // Should be accessible from the enum as if it was declared inside.
            console.log('Stopping...');
        }

        this.status = requestedStatus;// Should work, since EntityStatus contains all the enum members that RequestedEntityStatus has.
    }

    public getStatusAsStatusRequest() : RequestedEntityStatus {
        if (this.status === EntityStatus.created || this.status === EntityStatus.archived) {
            throw new Error('Invalid status');
        }
        return this.status as RequestedEntityStatus; // We have  eliminated the cases where the conversion is impossible, so the conversion should be possible now.
    }
}

Plus généralement, cela devrait fonctionner :

enum A { a = 'a' }
enum B { a = 'a' }

const a:A = A.a;
const b:B = B.a;

console.log(a === b);// Should not say "This condition will always return 'false' since the types 'A' and 'B' have no overlap.". They do have overlaps

Autrement dit

J'aimerais assouplir les contraintes sur les types enum pour qu'ils se comportent davantage comme des unions ( 'a' | 'b' ) que des structures opaques.

En ajoutant ces capacités au compilateur, deux énumérations indépendantes avec les mêmes valeurs peuvent être assignées l'une à l'autre avec les mêmes règles que les unions :
Étant donné les énumérations suivantes :

enum A { a = 'a', b = 'b' }
enum B { a = 'a' , c = 'c' }
enum C { ...A, c = 'c' }

Et trois variables a:A , b:B et c:C

  • c = a devrait fonctionner, car A n'est qu'un sous-ensemble de C, donc chaque valeur de A est une valeur valide de C
  • c = b devrait fonctionner, puisque B est juste 'a' | 'c' qui sont les deux valeurs valides de C
  • b = a pourrait fonctionner si a est connu pour être différent de 'b' (qui serait égal au type 'a' seulement)
  • a = b , de même, pourrait fonctionner si b est connu pour être différent de 'c'
  • b = c pourrait fonctionner si c est connu pour être différent de 'b' (ce qui équivaudrait à 'a'|'c' , ce qui est exactement ce que B est)

ou peut-être devrions-nous avoir besoin d'une distribution explicite sur les membres de droite, comme pour la comparaison d'égalité ?

À propos des conflits de membres enum

Je ne suis pas fan de la règle des "derniers gains", même si cela semble naturel avec l'opérateur de propagation.
Je dirais que le compilateur devrait renvoyer une erreur si une "clé" ou une "valeur" de l'énumération est dupliquée, à moins que la clé et la valeur ne soient identiques :

enum A { a = 'a', b = 'b' }
enum B { a = 'a' , c = 'c' }
enum C {...A, ...B } // OK, equivalent to enum C { a = 'a', b = 'b', c = 'c' }, a is deduplicated
enum D {...A, a = 'd' } // Error : Redefinition of a
enum E {...A, d = 'a' } // Error : Duplicate key with value 'a'

Fermeture

Je trouve cette proposition assez flexible et naturelle pour les développeurs TS, tout en permettant une plus grande sécurité de type (par rapport à as unknown as T ) sans réellement changer ce qu'est une énumération. Il introduit simplement une nouvelle façon d'ajouter des membres à l'énumération et une nouvelle façon de comparer les énumérations les unes aux autres.
Qu'est-ce que tu penses? Ai-je raté un problème d'architecture de langage évident qui rend cette fonctionnalité irréalisable ?

Cette page vous a été utile?
0 / 5 - 0 notes