Typescript: Stringbasierte Aufzählungen erweitern

Erstellt am 3. Aug. 2017  ·  68Kommentare  ·  Quelle: microsoft/TypeScript

Vor String-basierten Enums würden viele auf Objekte zurückgreifen. Die Verwendung von Objekten ermöglicht auch die Erweiterung von Typen. Zum Beispiel:

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

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

Beim Umschalten auf String-Enumerationen ist dies ohne eine Neudefinition der Enums nicht zu erreichen.

Ich wäre sehr nützlich, wenn ich so etwas tun könnte:

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

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

In Anbetracht der Tatsache, dass die erzeugten Aufzählungen Objekte sind, wird dies auch nicht allzu schlimm sein:

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

Hilfreichster Kommentar

Alle Problemumgehungen sind nett, aber ich würde gerne die Unterstützung der Enum-Vererbung von Typoskript selbst sehen, damit ich so einfach wie möglich umfassende Prüfungen durchführen kann.

Alle 68 Kommentare

Ich habe gerade ein bisschen damit gespielt und es ist derzeit möglich, diese Erweiterung mit einem Objekt für den erweiterten Typ durchzuführen, also sollte dies gut funktionieren:

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

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

Beachten Sie, dass Sie mit nahe kommen können

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

Eine weitere Option, abhängig von Ihren Anforderungen, ist die Verwendung eines Union-Typs:

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

let e: Events = AdvEvents.Pause;

Der Nachteil ist, dass Sie Events.Pause nicht verwenden können; Sie müssen AdvEvents.Pause verwenden. Wenn Sie const enums verwenden, ist dies wahrscheinlich in Ordnung. Andernfalls ist es für Ihren Anwendungsfall möglicherweise nicht ausreichend.

Wir brauchen diese Funktion für stark typisierte Redux-Reduzierer. Bitte fügen Sie es in TypeScript hinzu.

Eine andere Problemumgehung besteht darin, keine Aufzählungen zu verwenden, sondern etwas zu verwenden, das wie eine Aufzählung aussieht:

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

Alle Problemumgehungen sind nett, aber ich würde gerne die Unterstützung der Enum-Vererbung von Typoskript selbst sehen, damit ich so einfach wie möglich umfassende Prüfungen durchführen kann.

Verwenden Sie einfach Klasse anstelle von Enum.

Ich habe das gerade ausprobiert.

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

Es muss einen besseren Weg geben, dies zu tun.

Warum ist das nicht schon eine Funktion? Keine bahnbrechenden Änderungen, intuitives Verhalten, mehr als 80 Personen, die aktiv nach dieser Funktion gesucht und diese gefordert haben – es scheint ein Kinderspiel zu sein.

Sogar das erneute Exportieren von Aufzählungen aus einer anderen Datei in einem Namespace ist wirklich seltsam, ohne Aufzählungen zu erweitern (und es ist unmöglich, die Aufzählung so erneut zu exportieren, dass sie immer noch Aufzählung und nicht Objekt und Typ ist):

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
Derzeit wird eine Problemumgehung verwendet, dies sollte jedoch eine native Aufzählungsfunktion sein.

Ich habe diese Ausgabe überflogen, um zu sehen, ob jemand die folgende Frage gestellt hat. (Sieht nicht so aus.)

Von OP:

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

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

Würden die Leute erwarten, dass AdvEvents BasicEvents zuweisbar ist? (Wie es zum Beispiel bei extends für Klassen der Fall ist.)

Wenn ja, wie gut passt das dann zu der Tatsache, dass Enum-Typen endgültig sein sollen und nicht erweitert werden können?

@masak toller Punkt. Das Feature, das die Leute hier wollen, ist definitiv nicht wie normal extends . BasicEvents sollte AdvEvents zuweisbar sein, nicht umgekehrt. Normal extends verfeinert einen anderen Typ, um spezifischer zu sein, und in diesem Fall möchten wir den anderen Typ erweitern, um mehr Werte hinzuzufügen, daher sollte eine benutzerdefinierte Syntax dafür wahrscheinlich nicht das Schlüsselwort extends verwenden, oder zumindest nicht die Syntax enum A extends B { verwenden.

In diesem Sinne hat mir der Vorschlag von OP gefallen, dies zu verbreiten.

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

Denn die Verbreitung bringt bereits die Erwartung mit sich, dass das Original oberflächlich in eine unverbundene Kopie geklont wird.

BasicEvents sollte AdvEvents zuweisbar sein, nicht umgekehrt.

Ich kann sehen, wie das in allen Fällen wahr sein könnte , aber ich bin mir nicht sicher, ob es in allen Fällen wahr sein sollte , wenn Sie verstehen, was ich meine. Fühlt sich an, als wäre es domänenabhängig und verlässt sich auf den Grund, warum diese Enum-Werte kopiert wurden.

Ich habe ein wenig mehr über Problemumgehungen nachgedacht, und wenn Sie von https://github.com/Microsoft/TypeScript/issues/17592#issuecomment -331491147 abgehen, können Sie es etwas besser machen, indem Sie auch Events im Wert definieren Platz:

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;

Aus meinen Tests geht hervor, dass Events.Start BasicEvents.Start interpretiert wird, sodass die Überprüfung der Vollständigkeit und die Verfeinerung der diskriminierten Union gut zu funktionieren scheinen. Die Hauptsache, die fehlt, ist, dass Sie Events.Pause nicht als Typliteral verwenden können; Sie brauchen AdvEvents.Pause . Sie können typeof Events.Pause verwenden und es wird zu AdvEvents.Pause , obwohl die Leute in meinem Team durch diese Art von Muster verwirrt waren und ich denke, in der Praxis würde ich AdvEvents.Pause bei der Verwendung empfehlen es als Typ.

(Dies ist der Fall, wenn Sie möchten, dass die Aufzählungstypen untereinander zuweisbar sind und nicht isolierte Aufzählungen. Aus meiner Erfahrung ist es am häufigsten, dass sie zuweisbar sein sollen.)

Ein weiterer Vorschlag (auch wenn er das ursprüngliche Problem nicht löst): Wie wäre es stattdessen mit Zeichenfolgenliteralen, um eine Typenvereinigung zu erstellen?

type BEs = "Start" | "Finish";

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

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

Also könnte die Lösung für unsere Probleme das sein?

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

Das Erweitern von Aufzählungen sollte eine Kernfunktion von TypeScript sein. Ich sag bloß'

@wottpal Wiederhole meine Frage von früher:

Wenn [Aufzählungen erweitert werden können], wie gut passt das dann zu der Tatsache, dass Aufzählungstypen endgültig sein sollen und nicht erweitert werden können?

Insbesondere scheint mir, dass die Totalitätsprüfung einer switch-Anweisung über einen Enum-Wert von der Nichterweiterbarkeit von Enums abhängt.

@masak Was? Nein, tut es nicht! Da Extended Enum ein breiterer Typ ist und nicht der ursprünglichen Enum zugeordnet werden kann, kennen Sie immer alle Werte jeder verwendeten Enum. Erweitern bedeutet in diesem Zusammenhang, eine neue Aufzählung zu erstellen, nicht die alte zu ändern.

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, Sie meinen also, dass extends hier tatsächlich eher eine _Kopieren_-Semantik hat (der Enum-Werte von A in B )? Dann, ja, die Schalter kommen in Ordnung.

Allerdings gibt es _irgendeine_ Erwartung darin, die mir immer noch gebrochen erscheint. Um es festzuhalten: Bei Klassen vermittelt extends _keine_ Kopiersemantik — Felder und Methoden werden nicht in die erweiternde Unterklasse kopiert; Stattdessen werden sie nur über die Prototypenkette bereitgestellt. Es gibt immer nur ein Feld oder eine Methode in der Oberklasse.

Aus diesem Grund ist class B extends A garantiert, dass B A zuweisbar ist , und so wäre beispielsweise let a: A = new B(); vollkommen in Ordnung.

Aber mit enums und extends könnten wir let a: A = B.b; nicht machen, weil es keine entsprechende Garantie gibt. Was sich für mich seltsam anfühlt; extends vermittelt hier eine Reihe von Annahmen darüber, was getan werden kann, und sie werden nicht mit Aufzählungen erfüllt.

Dann einfach expands oder clones nennen? 🤷‍♂️
Aus Benutzersicht fühlt es sich einfach seltsam an, dass etwas so Grundlegendes nicht einfach zu erreichen ist.

Wenn die vernünftige Semantik ein ganz neues Schlüsselwort erfordert (ohne viel Stand der Technik in anderen Sprachen), warum nicht stattdessen die Spread-Syntax ( ... ) wiederverwenden, wie in OP und diesem Kommentar vorgeschlagen?

+1 Ich hoffe, dass dies zur standardmäßigen Aufzählungsfunktion hinzugefügt wird. :)

Kennt jemand elegante Lösungen??? 🧐

Wenn die vernünftige Semantik ein ganz neues Schlüsselwort erfordert (ohne viel Stand der Technik in anderen Sprachen), warum nicht stattdessen die Spread-Syntax (...) wiederverwenden, wie in OP und diesem Kommentar vorgeschlagen?

Ja, nachdem ich ein bisschen darüber nachgedacht habe, denke ich, dass diese Lösung gut wäre.

Nach dem Lesen dieses ganzen Problem-Threads scheint es eine breite Übereinstimmung darüber zu geben, dass die Wiederverwendung des Spread-Operators das Problem löst und alle Bedenken anspricht, die von Leuten geäußert wurden, die Syntax verwirrend/nicht intuitiv zu machen.

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

Benötigt diese Ausgabe an dieser Stelle wirklich noch das Label „Warten auf mehr Feedback“, @RyanCavanaugh ?

Die Funktion wollte +1

Haben wir Neuigkeiten zu diesem Thema? Es fühlt sich wirklich nützlich an, den Spreading-Operator für Aufzählungen implementiert zu haben.

Besonders für Anwendungsfälle, die Metaprogrammierung beinhalten, ist die Fähigkeit, Enumerationen zu aliasieren und zu erweitern, irgendwo zwischen einem Muss und einem Nice-to-Have. Es gibt derzeit keine Möglichkeit, eine Aufzählung zu nehmen und export unter einem anderen Namen zu verwenden - es sei denn, Sie greifen auf eine der oben genannten Problemumgehungen zurück.

@m93a Ah, Sie meinen also, dass extends hier tatsächlich eher eine _Kopieren_-Semantik hat (der Enum-Werte von A in B )? Dann, ja, die Schalter kommen in Ordnung.

Allerdings gibt es _irgendeine_ Erwartung darin, die mir immer noch gebrochen erscheint. Um es festzuhalten: Bei Klassen vermittelt extends _keine_ Kopiersemantik — Felder und Methoden werden nicht in die erweiternde Unterklasse kopiert; Stattdessen werden sie nur über die Prototypenkette bereitgestellt. Es gibt immer nur ein Feld oder eine Methode in der Oberklasse.

Aus diesem Grund ist class B extends A garantiert, dass B A zuweisbar ist , und so wäre beispielsweise let a: A = new B(); vollkommen in Ordnung.

Aber mit enums und extends könnten wir let a: A = B.b; nicht machen, weil es keine entsprechende Garantie gibt. Was sich für mich seltsam anfühlt; extends vermittelt hier eine Reihe von Annahmen darüber, was getan werden kann, und sie werden nicht mit Aufzählungen erfüllt.

@masak Ich denke, Sie sind fast richtig, aber Sie haben eine kleine Annahme getroffen, die falsch ist. B ist A im Falle von enum B extends A zuweisbar, da "zuweisbar" bedeutet, dass alle von A bereitgestellten Werte in B verfügbar sind. Wenn Sie sagten, dass let a: A = B.b , gehen Sie davon aus, dass Werte in B sein müssen verfügbar in A, was nicht dasselbe ist wie die Werte, die A zuweisbar sind. let a: A = B.a IST richtig, weil B A zuweisbar ist.

Dies wird anhand von Klassen wie im folgenden Beispiel deutlich:

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`

TypeScript Playground-Link

invalid access

Um es kurz zu machen, ich glaube, Extended IS ist die richtige Terminologie, da genau das getan wird. Im enum B extends A -Beispiel können Sie IMMER erwarten, dass eine B-Enumeration alle möglichen Werte der A-Enumeration enthält, da B eine "Unterklasse" (Subenum? vielleicht gibt es ein besseres Wort dafür) von A und somit A zuordenbar.

Ich glaube also nicht, dass wir ein neues Schlüsselwort brauchen, ich denke, wir sollten Extends verwenden UND ich denke, dies sollte ein nativer Teil von TypeScript sein: D

@julian-sf Ich glaube, ich stimme allem zu, was du geschrieben hast ...

...aber... :slightly_smiling_face:

Wie ich hier problematisiert habe , was ist mit dieser Situation?

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

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

Angesichts der Tatsache, dass Pause eine _Instanz_ von AdvEvents ist und AdvEvents _erweitert_ BasicEvents , würden Sie auch erwarten, dass Pause eine _Instanz_ von BasicEvents ist

Auf der anderen Seite besteht das Kernwertversprechen von Aufzählungen (IMHO) darin, dass sie _geschlossen_/"final" (wie in, nicht erweiterbar) sind, sodass so etwas wie eine switch -Anweisung die Gesamtheit annehmen kann. (Und so AdvEvents in der Lage zu sein, _erweitern_ zu können, was es bedeutet, ein BasicEvent zu sein, verstößt gegen eine Art geringste Überraschung für Aufzählungen.)

Ich glaube nicht, dass Sie mehr als zwei der folgenden drei Eigenschaften können:

  • Aufzählungen sind geschlossen/endgültig/vorhersagbar gesamt
  • Eine extends Beziehung zwischen zwei enum -Deklarationen
  • Die (vernünftige) Annahme, dass, wenn b eine Instanz von B und B extends A ist, dann b eine Instanz von A ist

@masak Ich verstehe und stimme dem geschlossenen Prinzip von Enumerationen (zur Laufzeit) zu. Aber die Erweiterung zur Kompilierzeit würde das geschlossene Prinzip zur Laufzeit nicht verletzen, da sie alle vom Compiler definiert und konstruiert würden.

Die (vernünftige) Annahme, dass, wenn b eine Instanz von B ist und B A erweitert, dann b eine Instanz von A ist

Ich denke, diese Argumentation ist irreführend, da die Instanz/Klassen-Dichotomie nicht wirklich der Aufzählung zugeordnet werden kann. Aufzählungen sind keine Klassen und haben keine Instanzen. Ich denke jedoch, dass sie erweiterbar sein können, wenn sie richtig gemacht werden. Stellen Sie sich Aufzählungen eher wie Mengen vor. In diesem Beispiel ist B eine Obermenge von A. Daher ist es vernünftig anzunehmen, dass jeder Wert in A in B vorhanden ist, aber dass nur EINIGE Werte von B in A vorhanden sein werden.

Ich verstehe, woher die Besorgnis kommt ... obwohl. Und ich bin mir nicht sicher, was ich dagegen tun soll. Ein gutes Beispiel für ein Problem mit der Enum-Erweiterung:

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

In diesem Fall verhalten sich Aufzählungen ganz anders als Klassen. Wenn dies Klassen wären, würden Sie erwarten, dass Sie B ganz einfach in A umwandeln können, aber das wird hier eindeutig nicht funktionieren. Ich denke nicht unbedingt, dass das SCHLECHT ist, ich denke, es sollte einfach berücksichtigt werden. IE, Sie können einen Aufzählungstyp nicht wie eine Klasse in seinem Vererbungsbaum nach oben verschieben. Dies könnte mit einem Compiler-Fehler in der Art von "kann Superset-Enumeration B nicht A zuweisen, da nicht alle Werte von B in A vorhanden sind" erklärt werden.

@julian-sf

Ich denke, diese Argumentation ist irreführend, da die Instanz/Klassen-Dichotomie nicht wirklich der Aufzählung zugeordnet werden kann. Aufzählungen sind keine Klassen und haben keine Instanzen.

Oberflächlich gesehen hast du völlig recht.

  • Als eigenständiges Sprachkonstrukt betrachtet, hat eine Aufzählung _Mitglieder_, keine Instanzen. Der Begriff "Mitglied" wird sowohl im Handbuch als auch in der Sprachspezifikation verwendet. (C# und Python verwenden in ähnlicher Weise den Begriff „Member“. Java verwendet „Enum-Konstante“, weil „Member“ in Java eine überladene Bedeutung hat.)
  • Aus der Perspektive des kompilierten Codes betrachtet, hat eine Aufzählung _Eigenschaften_, die in beide Richtungen abgebildet werden – Namen-zu-Werte und Werte-zu-Namen. Wieder keine Instanzen.

Wenn ich darüber nachdenke, stelle ich fest, dass ich ein wenig von Javas Interpretation von Enums gefärbt bin. In Java sind Aufzählungswerte buchstäblich Instanzen ihres Aufzählungstyps. Hinsichtlich der Implementierung ist eine Aufzählung eine Klasse, die die Aufzählungsklasse erweitert. (Sie dürfen das nicht manuell tun , Sie müssen das Schlüsselwort enum durchgehen, aber das passiert unter der Haube.) Das Schöne daran ist, dass Aufzählungen alle Annehmlichkeiten haben, die Klassen haben: sie kann Felder, Konstruktoren, Methoden haben... In diesem Ansatz sind Aufzählungsmitglieder _Instanzen_. (Das sagt auch die JLS.)

Beachten Sie, dass ich keine Änderungen an der TypeScript-Enumerationssemantik vorschlage. Insbesondere sage ich nicht, dass TypeScript auf die Verwendung des Java-Modells für Aufzählungen umstellen sollte. Ich _am_ sage, dass es lehrreich/aufschlussreich ist, ein Klassen-/Instanz-„Verständnis“ über Aufzählungen/Enum-Mitgliedern zu legen. Nicht "eine Aufzählung _ist_ eine Klasse" oder "ein Aufzählungsmitglied _ist_ eine Instanz" ... aber es gibt Ähnlichkeiten, die übertragen werden.

Welche Ähnlichkeiten? Geben Sie in erster Linie Mitgliedschaft ein.

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

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

Die letzte Zeile wird nicht typgeprüft, da Bar.Z kein Foo ist. Auch dies sind nicht _eigentlich_ Klassen und Instanzen, aber es kann mit demselben Modell _verstanden_ werden, als ob Foo und Bar Klassen wären und die sechs Mitglieder ihre jeweiligen Instanzen wären.

(Wir ignorieren für die Zwecke dieses Arguments die Tatsache, dass let foo: Foo = 2; auch semantisch zulässig ist und dass im Allgemeinen number -Werte Variablen vom Typ enum zuweisbar sind.)

Aufzählungen haben die zusätzliche Eigenschaft, dass sie _geschlossen_ sind – sorry, ich kenne keinen besseren Ausdruck dafür – wenn Sie sie einmal definiert haben, können Sie sie nicht mehr erweitern. Insbesondere sind die innerhalb der Enum-Deklaration aufgelisteten Member die _einzigen_ Dinge, die mit dem Enum-Typ übereinstimmen. ("Geschlossen" wie in "Geschlossene-Welt-Hypothese".) Dies ist eine großartige Eigenschaft, da Sie mit absoluter Sicherheit überprüfen können, ob alle Fälle in einer switch -Anweisung in Ihrer Aufzählung abgedeckt wurden.

Mit extends auf Enums verschwindet diese Eigenschaft.

Du schreibst,

Ich verstehe und stimme dem geschlossenen Prinzip von Enumerationen (zur Laufzeit) zu. Aber die Erweiterung zur Kompilierzeit würde das geschlossene Prinzip zur Laufzeit nicht verletzen, da sie alle vom Compiler definiert und konstruiert würden.

Ich glaube nicht, dass das stimmt, weil davon ausgegangen wird, dass sich jeder Code, der Ihre Aufzählung erweitert, in Ihrem Projekt befindet. Aber ein Modul eines Drittanbieters kann Ihre Aufzählung erweitern, und plötzlich gibt es _neue_ Aufzählungsmitglieder, die Ihrer Aufzählung ebenfalls zuweisbar sind, außerhalb der Kontrolle des von Ihnen kompilierten Codes. Im Wesentlichen würden Aufzählungen nicht mehr geschlossen, nicht einmal zur Kompilierzeit.

Ich fühle mich immer noch etwas ungeschickt darin, genau das auszudrücken, was ich meine, aber ich glaube, es ist wichtig: extends auf enum würde eines der wertvollsten Merkmale von Aufzählungen brechen, die Tatsache, dass sie wieder geschlossen. Bitte zählen Sie, wie viele Sprachen aus genau diesem Grund das Erweitern/Unterklassifizieren einer Aufzählung absolut _verbieten_.

Ich habe etwas mehr über Problemumgehungen nachgedacht, und wenn Sie #17592 (Kommentar) abarbeiten , können Sie es etwas besser machen, indem Sie auch Events im Wertebereich definieren:

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;

Aus meinen Tests geht hervor, dass Events.Start BasicEvents.Start interpretiert wird, sodass die Überprüfung der Vollständigkeit und die Verfeinerung der diskriminierten Union gut zu funktionieren scheinen. Die Hauptsache, die fehlt, ist, dass Sie Events.Pause nicht als Typliteral verwenden können; Sie brauchen AdvEvents.Pause . Sie _können_ typeof Events.Pause verwenden und es wird zu AdvEvents.Pause aufgelöst, obwohl die Leute in meinem Team durch diese Art von Muster verwirrt waren und ich denke, in der Praxis würde ich AdvEvents.Pause bei der Verwendung empfehlen es als Typ.

(Dies ist der Fall, wenn Sie möchten, dass die Aufzählungstypen untereinander zuweisbar sind und nicht isolierte Aufzählungen. Aus meiner Erfahrung ist es am häufigsten, dass sie zuweisbar sein sollen.)

Ich denke, das ist im Moment die sauberste Lösung zur Hand.

Danke @alangpierce :+1:

Gibt es hierzu Neuigkeiten?

@sdwvit Ich gehöre nicht zu den Kernleuten, aber aus meiner Sicht würde der folgende Syntaxvorschlag (von OP, aber danach zweimal erneut vorgeschlagen) alle glücklich machen, ohne bekannte Probleme:

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

Es würde _mich_ glücklich machen, weil es bedeuten würde, dieses scheinbar nützliche Feature "alle Mitglieder in dieser anderen Aufzählung duplizieren" _ohne_ mit extends zu implementieren, was ich aus den genannten Gründen für problematisch halte. Die ... -Syntax vermeidet diese Probleme, indem sie kopiert und nicht erweitert wird.

Das Problem ist immer noch als "Warten auf weitere Rückmeldungen" gekennzeichnet, und ich respektiere das Recht der Kernmitglieder, es so lange in dieser Kategorie zu belassen, wie sie es für notwendig halten. Aber auch nichts hindert irgendjemanden daran, das Obige zu implementieren und als PR einzureichen.

@masak danke für die Antwort. Ich muss jetzt die ganze Diskussionsgeschichte durchgehen. Melde mich danach wieder :)

Ich würde dies sehr gerne sehen und würde sehr gerne versuchen, dies selbst umzusetzen. Wir müssen jedoch noch Verhaltensweisen für alle Aufzählungen definieren. Das alles funktioniert gut für String-basierte Aufzählungen, aber was ist mit numerischen Vanilla-Aufzählungen? Wie funktioniert hier das Erweitern/Kopieren?

  • Ich nehme an, wir wollen nur zulassen, dass eine Aufzählung mit einer Aufzählung vom gleichen Typ erweitert wird (numerisch erweitert numerisch, Zeichenfolge erweitert Zeichenfolge). Heterogene Aufzählungen werden technisch unterstützt, also sollten wir diese Unterstützung beibehalten.

  • Sollten wir das Erweitern von mehreren Aufzählungen zulassen? Sollten sie alle sich gegenseitig ausschließende Werte haben? Oder werden wir überlappende Werte zulassen? Priorität basierend auf lexikalischer Reihenfolge?

  • Können die erweiterten Aufzählungen Werte der erweiterten Aufzählungen überschreiben?

  • Müssen die erweiterten Aufzählungen am Anfang der Werteliste erscheinen oder können sie in beliebiger Reihenfolge sein? Ich nehme an, später definierte Werte haben höhere Priorität?

  • Ich gehe davon aus, dass implizite numerische Werte nach dem Maximalwert der erweiterten numerischen Aufzählungen mit 1 fortgesetzt werden.

  • Besondere Überlegungen für Bitmasken?

usw. usw.

@JeffreyMercado Dies sind gute Fragen und geeignet für jemanden, der hofft, eine Implementierung zu versuchen. :Lächeln:

Im Folgenden finden Sie meine Antworten, die von einem "konservativen" Designansatz geleitet werden (wie in "Lasst uns Designentscheidungen treffen, die die Fälle verbieten, bei denen wir uns nicht sicher sind, anstatt jetzt Entscheidungen zu treffen, die später schwer zu ändern sind, während sie abwärtskompatibel bleiben").

  • Ich nehme an, wir wollen nur zulassen, dass eine Aufzählung mit einer Aufzählung vom gleichen Typ erweitert wird (numerisch erweitert numerisch, Zeichenfolge erweitert Zeichenfolge).

Das vermute ich auch. Die resultierende Aufzählung des gemischten Typs scheint nicht sehr nützlich zu sein.

  • Sollten wir das Erweitern von mehreren Aufzählungen zulassen? Sollten sie alle sich gegenseitig ausschließende Werte haben? Oder werden wir überlappende Werte zulassen? Priorität basierend auf lexikalischer Reihenfolge?

Da es sich hier um das Kopieren von Semantik handelt, scheint das Duplizieren mehrerer Enums "besser in Ordnung" zu sein als Multiple Inheritance à la C++. Ich sehe darin nicht sofort ein Problem, besonders wenn wir weiter auf der Analogie der Objektverteilung aufbauen: let newEnum = { ...enumA, ...enumB };

Sollten alle Mitglieder sich gegenseitig ausschließende Werte haben? Das Konservative wäre, „Ja“ zu sagen. Auch hier liefert uns die Analogie der Objektverbreitung eine alternative Semantik: Der Letzte gewinnt.

Ich kann mir keine Anwendungsfälle vorstellen, in denen ich es begrüßen würde, Enum-Werte überschreiben zu können. Aber das ist vielleicht nur ein Mangel an Fantasie meinerseits. Der konservative Ansatz, Kollisionen zu verbieten, hat die erfreulichen Eigenschaften, dass er leicht zu erklären/verinnerlichen ist und zumindest theoretisch echte Designfehler aufdecken könnte (in frischem Code oder Code, der gewartet wird).

  • Können die erweiterten Aufzählungen Werte der erweiterten Aufzählungen überschreiben?

Ich denke, die Antwort und die Argumentation sind in diesem Fall ähnlich wie im vorherigen Fall.

  • Müssen die erweiterten Aufzählungen am Anfang der Werteliste erscheinen oder können sie in beliebiger Reihenfolge sein? Ich nehme an, später definierte Werte haben höhere Priorität?

Ich wollte zuerst sagen, dass dies nur dann von Bedeutung ist, wenn wir uns an die Semantik des Überschreibens „der Letzte gewinnt“ halten.

Aber beim zweiten Nachdenken, sowohl unter "keine Kollisionen" als auch unter "letzter gewinnt", finde ich es seltsam, sogar ENUM-Member-Deklarationen zu platzieren, bevor ENUM-Spreads in der Liste erscheinen. Welche Absicht wird damit kommuniziert? Die Spreads sind ein bisschen wie „Importe“, und diese gehen herkömmlicherweise an die Spitze.

Ich möchte nicht unbedingt verbieten, Enum-Spreads nach Enum-Member-Deklarationen zu platzieren (obwohl ich denke, dass ich damit einverstanden wäre, wenn dies in der Grammatik nicht zulässig wäre). Wenn es am Ende erlaubt ist, ist es definitiv etwas, auf das Linters und Community-Konventionen als vermeidbar hinweisen könnten. Es gibt einfach keinen zwingenden Grund dafür.

  • Ich gehe davon aus, dass implizite numerische Werte nach dem Maximalwert der erweiterten numerischen Aufzählungen mit 1 fortgesetzt werden.

Vielleicht ist es konservativ, einen expliziten Wert für das erste Mitglied nach einem Spread zu fordern.

  • Besondere Überlegungen für Bitmasken?

Ich denke, das würde durch die obige Regel abgedeckt werden.

Ich konnte etwas Vernünftiges tun, indem ich Aufzählungen, Schnittstellen und unveränderliche Objekte kombinierte.

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 Das ist eine nette Verwendung von Aufzählungen, aber ... Ich verstehe nicht, wie es als Erweiterung einer vorhandenen Aufzählung zählt. Was Sie tun, ist die Aufzählung _use_.

(Im Gegensatz zu Enums und Switch-Anweisungen scheint es in Ihrem Beispiel auch keine Totalitätsprüfung zu geben; jemand, der später ein Enum-Mitglied hinzugefügt hat, könnte leicht vergessen, einen entsprechenden Schlüssel in den SINGULAR und PLURAL hinzuzufügen Label .)

@masak

jemand, der später ein Enum-Mitglied hinzugefügt hat, könnte leicht vergessen, einen entsprechenden Schlüssel in den SINGULAR - und PLURAL -Einträgen in allen Instanzen von Label hinzuzufügen.)

Zumindest in meiner Umgebung wird ein Fehler ausgegeben, wenn ein Aufzählungsmitglied entweder in SINGULAR oder in PLURAL fehlt. Der Typ Record erfüllt seinen Zweck, denke ich.

Obwohl die Dokumentation für TS gut ist , habe ich das Gefühl, dass es nicht viele Beispiele dafür gibt, wie viele Funktionen auf nicht triviale Weise miteinander kombiniert werden können. enum inheritance war das erste, was ich nachgeschlagen habe, als ich versuchte, Internationalisierungsprobleme zu lösen, was zu diesem Thread führte. Der Ansatz stellte sich ohnehin als falsch heraus, weshalb ich diesen Beitrag geschrieben habe.

@illeatmyhat

Zumindest in meiner Umgebung wird ein Fehler ausgegeben, wenn ein Aufzählungsmitglied entweder in SINGULAR oder in PLURAL fehlt. Der Typ Record erfüllt seinen Zweck, denke ich.

Oh! BIS. Und ja, das macht es viel interessanter. Ich verstehe, was Sie meinen, wenn Sie zunächst nach Enum-Vererbung greifen und schließlich auf Ihrem Muster landen. Das ist vielleicht nicht einmal eine isolierte Sache; „X/Y-Probleme“ sind eine reale Sache. Mehr Leute beginnen vielleicht mit dem Gedanken „Ich möchte MyEnum verlängern“, aber am Ende verwenden sie Record<MyEnum, string> , wie Sie es getan haben.

Antwort an @masak :

Mit Erweiterungen auf Aufzählungen verschwindet diese Eigenschaft aus dem Fenster.

Du schreibst,

@julian-sf: Ich verstehe und stimme dem geschlossenen Prinzip von Enumerationen (zur Laufzeit) zu. Aber die Erweiterung zur Kompilierzeit würde das geschlossene Prinzip zur Laufzeit nicht verletzen, da sie alle vom Compiler definiert und konstruiert würden.

Ich glaube nicht, dass das stimmt, weil davon ausgegangen wird, dass sich jeder Code, der Ihre Aufzählung erweitert, in Ihrem Projekt befindet. Aber ein Modul eines Drittanbieters kann Ihre Aufzählung erweitern, und plötzlich gibt es neue Aufzählungsmitglieder, die Ihrer Aufzählung ebenfalls zuweisbar sind, außerhalb der Kontrolle des von Ihnen kompilierten Codes. Im Wesentlichen würden Aufzählungen nicht mehr geschlossen, nicht einmal zur Kompilierzeit.

Je mehr ich darüber nachdenke, da hast du völlig recht. Aufzählungen sollten geschlossen werden. Ich mag die Idee, Aufzählungen zu "komponieren", da ich denke, dass dies wirklich der Kern der Sache ist, die wir hier wollen 🥳.

Ich denke, diese Notation fasst das Konzept des "Zusammenfügens" zweier separater Aufzählungen ziemlich elegant zusammen:

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

Bedenken Sie also, dass mein Verzicht auf die Verwendung des Begriffs extends 😆


Kommentare zu den Antworten von @masak auf die Fragen von @JeffreyMercado :

  • Ich nehme an, wir wollen nur zulassen, dass eine Aufzählung mit einer Aufzählung vom gleichen Typ erweitert wird (numerisch erweitert numerisch, Zeichenfolge erweitert Zeichenfolge). Heterogene Aufzählungen werden technisch unterstützt, also sollten wir diese Unterstützung beibehalten.

Das vermute ich auch. Die resultierende Aufzählung des gemischten Typs scheint nicht sehr nützlich zu sein.

Obwohl ich zustimme, dass es nicht nützlich ist, SOLLTEN wir hier wahrscheinlich eine heterogene Unterstützung für Aufzählungen beibehalten. Ich denke, eine Linter-Warnung wäre hier nützlich, aber ich denke nicht, dass TS dem im Wege stehen sollte. Ich kann mir einen erfundenen Anwendungsfall vorstellen, der lautet, ich baue eine Aufzählung für Interaktionen mit einer sehr schlecht gestalteten API, die Flags akzeptiert, die eine Mischung aus Zahlen und Zeichenfolgen sind. Ausgedacht, ich weiß, aber da es anderswo erlaubt ist, denke ich nicht, dass wir es hier verbieten sollten.

Vielleicht nur eine starke Ermutigung, es nicht zu tun?

  • Sollten wir das Erweitern von mehreren Aufzählungen zulassen? Sollten sie alle sich gegenseitig ausschließende Werte haben? Oder werden wir überlappende Werte zulassen? Priorität basierend auf lexikalischer Reihenfolge?

Da es sich hier um das Kopieren von Semantik handelt, scheint das Duplizieren mehrerer Enums "besser in Ordnung" zu sein als Multiple Inheritance à la C++. Ich sehe darin nicht sofort ein Problem, besonders wenn wir weiter auf der Analogie der Objektverteilung aufbauen: let newEnum = { ...enumA, ...enumB };

100% einverstanden

  • Sollten alle Mitglieder sich gegenseitig ausschließende Werte haben?

Das Konservative wäre, „Ja“ zu sagen. Auch hier liefert uns die Analogie der Objektverbreitung eine alternative Semantik: Der Letzte gewinnt.

Ich bin hier zerrissen. Ich stimme zwar zu, dass es "beste Praxis" ist, die gegenseitige Ausschließlichkeit von Werten zu erzwingen, aber ist es richtig? Es steht in direktem Widerspruch zu allgemein bekannter Spread-Semantik. Einerseits gefällt mir die Idee, sich gegenseitig ausschließende Werte durchzusetzen, andererseits bricht es mit vielen Annahmen darüber, wie eine verbreitete Semantik funktionieren sollte. Gibt es Nachteile beim Befolgen normaler Streuregeln mit „Der Letzte gewinnt“? Es scheint, als wäre die Implementierung einfacher (da das zugrunde liegende Objekt sowieso nur eine Karte ist). Aber es scheint auch mit den allgemeinen Erwartungen übereinzustimmen. Ich neige dazu, weniger überraschend zu sein.

Es kann auch gute Beispiele dafür geben, wie man einen Wert überschreiben möchte (obwohl ich keine Ahnung habe, was das wäre).

Aber beim zweiten Nachdenken, sowohl unter "keine Kollisionen" als auch unter "letzter gewinnt", finde ich es seltsam, überhaupt Enum-Member-Deklarationen setzen zu wollen, bevor sich Enum in der Liste ausbreitet. Welche Absicht wird damit kommuniziert? Die Spreads sind ein bisschen wie „Importe“, und diese gehen herkömmlicherweise an die Spitze.

Nun, das hängt davon ab, ob wir der Spread-Semantik folgen, dann sollte es keine Rolle spielen, wie die Reihenfolge lautet. Ehrlich gesagt, selbst wenn wir Werte durchsetzen, die sich gegenseitig ausschließen, spielt die Reihenfolge keine Rolle, oder? Eine Kollision wäre an diesem Punkt unabhängig von der Reihenfolge ein Fehler.

  • Ich gehe davon aus, dass implizite numerische Werte nach dem Maximalwert der erweiterten numerischen Aufzählungen mit 1 fortgesetzt werden.

Vielleicht ist es konservativ, einen expliziten Wert für das erste Mitglied nach einem Spread zu fordern.

Ich stimme zu. Wenn Sie eine Aufzählung verteilen, sollte TS nur explizite Werte für zusätzliche Mitglieder erzwingen.

@julian-sf

Bedenkt also, dass sich mein Verzicht auf die Nutzung des Begriffs erstreckt 😆

:+1: Die Gesellschaft zur Erhaltung von Summentypen jubelt von der Seitenlinie.

Aber beim zweiten Nachdenken, sowohl unter "keine Kollisionen" als auch unter "letzter gewinnt", finde ich es seltsam, überhaupt Enum-Member-Deklarationen setzen zu wollen, bevor sich Enum in der Liste ausbreitet. Welche Absicht wird damit kommuniziert? Die Spreads sind ein bisschen wie „Importe“, und diese gehen herkömmlicherweise an die Spitze.

Nun, das hängt davon ab, ob wir der Spread-Semantik folgen, dann sollte es keine Rolle spielen, wie die Reihenfolge lautet. Ehrlich gesagt, selbst wenn wir Werte durchsetzen, die sich gegenseitig ausschließen, spielt die Reihenfolge keine Rolle, oder? Eine Kollision wäre an diesem Punkt unabhängig von der Reihenfolge ein Fehler.

Ich sage "es gibt keinen guten Grund, Spreads nach normalen Mitgliedserklärungen zu platzieren"; Sie sagen "unter den entsprechenden Einschränkungen macht es keinen Unterschied, ob Sie sie davor oder danach platzieren". Beides kann gleichzeitig wahr sein.

Der Hauptunterschied im Ergebnis scheint in einem Spektrum zu liegen, in dem Spreads vor normalen Mitgliedern zugelassen oder nicht zugelassen werden. Es könnte syntaktisch verboten werden; es könnte eine Linter-Warnung erzeugen; oder es könnte in jeder Hinsicht völlig in Ordnung sein. Wenn die Reihenfolge keinen semantischen Unterschied macht, kommt es darauf an, dass die ENUM-Spread-Funktion dem Prinzip der geringsten Überraschung folgt, einfach zu verwenden und leicht zu lehren/erklären ist.

Die Verwendung des Spread-Operators fällt in die breitere Verwendung von flachem Kopieren in JS und TypeScript. Es ist sicherlich die weiter verbreitete und einfacher zu verstehende Methode als die Verwendung extends , was eine direkte Beziehung impliziert. Das Erstellen einer Aufzählung durch Komposition wäre die einfacher zu verwendende Lösung.

Einige der Workaround-Vorschläge sind zwar gültig und verwendbar, fügen jedoch viel mehr Boilerplate-Code hinzu, um das gleiche gewünschte Ergebnis zu erzielen. Angesichts der endgültigen und unveränderlichen Natur einer Aufzählung wäre es wünschenswert, zusätzliche Aufzählungen durch Komposition zu erstellen, um die Eigenschaften beizubehalten, die mit anderen Sprachen konsistent sind.

Es ist nur eine Schande, dass 3 Jahre an diesem Gespräch noch andauern.

@jmitchell38488 Ich würde deinen Kommentar mit „Gefällt mir“ markieren, aber dein letzter Satz hat meine Meinung geändert. Dies ist eine notwendige Diskussion, da die vorgeschlagene Lösung funktionieren würde, aber es impliziert auch die Möglichkeit, Klassen und Schnittstellen auf diese Weise zu erweitern. Es ist eine große Änderung, die einige C++-ähnliche Programmierer davon abhalten kann, Typoskript zu verwenden, da Sie im Grunde zwei Möglichkeiten haben, dasselbe zu tun ( class A extends B und class A { ...(class B {}) } ). Ich denke, beide Wege können unterstützt werden, aber dann brauchen wir extend auch für Enums aus Gründen der Konsistenz.

@masak wdyt? ^

@sdwvit Ich möchte nicht das Verhalten zum Erstellen von Klassen und Schnittstellen ändern, ich spreche speziell von Aufzählungen und deren Erstellung durch Komposition. Sie sind unveränderliche endgültige Typen, daher sollten wir nicht in der Lage sein, in der typischen Vererbungsart zu erweitern.

Angesichts der Natur von JS und des transpilierten Endwerts gibt es keinen Grund, warum die Komposition nicht erreicht werden kann. Sicher würde die Arbeit mit Aufzählungen attraktiver machen.

@masak wdyt? ^

Hm. Ich denke, Sprachkonsistenz ist ein lobenswertes Ziel, und deshalb ist es nicht _a priori_ falsch, nach einem ähnlichen ... -Feature in Klassen und Interfaces zu fragen. Aber ich denke, der Fall ist dort schwächer oder nicht vorhanden, und zwar aus zwei Gründen: (a) Klassen und Schnittstellen haben bereits einen Erweiterungsmechanismus, und das Hinzufügen eines zweiten bietet wenig zusätzlichen Wert (während das Bereitstellen eines für Aufzählungen einen Anwendungsfall abdecken würde, den Menschen kommen seit Jahren auf dieses Thema zurück); (b) Das Hinzufügen neuer Syntax und Semantik zu Klassen hat eine viel höhere Zulassungsschwelle, da Klassen in gewissem Sinne aus der EcmaScript-Spezifikation stammen. (TypeScript ist älter als ES6, aber es wurde aktiv versucht, dass ersteres nahe an letzterem bleibt. Dazu gehört, dass keine zusätzlichen Funktionen hinzugefügt werden.)

Ich denke, dieser Thread ist schon lange offen, einfach weil er ein würdiges Feature darstellt, das tatsächliche Anwendungsfälle abdecken würde, aber es muss noch eine PR dafür erstellt werden. Das Erstellen einer solchen PR erfordert Zeit und Mühe, mehr als nur zu sagen, dass Sie die Funktion möchten. :zwinkern:

Arbeitet jemand an dieser Funktion?

Arbeitet jemand an dieser Funktion?

Meine Vermutung ist nein, da wir die Diskussion darüber noch nicht einmal beendet haben, haha!

Ich habe das Gefühl, dass wir einem Konsens darüber, wie dies aussehen würde, näher gekommen sind. Da dies eine Spracherweiterung wäre, würde es wahrscheinlich einen "Champion" erfordern, um diesen Vorschlag voranzutreiben. Ich denke, jemand aus dem TS-Team müsste hereinkommen und das Problem von "Warten auf Feedback" auf "Warten auf Vorschlag" (oder etwas Ähnliches) verschieben.

Ich arbeite an einem Prototyp davon. Obwohl ich aufgrund von Zeitmangel und Unkenntnis der Struktur des Codes nicht sehr weit gekommen bin. Ich möchte das durchziehen und wenn es keine andere Bewegung gibt, werde ich weitermachen, wenn ich kann.

Würde diese Funktion auch lieben. 37 Monate sind vergangen, hoffentlich geht es bald voran

Notizen vom letzten Treffen:

  • Wir mögen die Spread-Syntax, weil extends einen Subtyp impliziert, während das „Erweitern“ einer Aufzählung einen Supertyp erzeugt.

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     ...BasicEvents,
     Pause = "Pause",
     Resume = "Resume"
    }
    
  • Der Gedanke ist, dass AdvEvents.Start in dieselbe Typidentität wie BasicEvents.Start werden würde. Dies hat zur Folge, dass die Typen BasicEvents.Start und AdvEvents.Start einander zuweisbar wären und der Typ BasicEvents AdvEvents zuweisbar wäre. Hoffentlich ergibt das einen intuitiven Sinn, aber es ist wichtig zu beachten, dass die Ausbreitung nicht nur eine syntaktische Abkürzung ist – wenn wir die Ausbreitung dahingehend erweitern, wie sie aussieht:

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

    Dies hat ein anderes Verhalten – hier sind BasicEvents.Start und AdvEvents.Start einander _nicht_ zuweisbar, aufgrund der undurchsichtigen Qualität von String-Enumerationen.

    Eine weitere geringfügige Konsequenz dieser Implementierung ist, dass AdvEvents.Start wahrscheinlich als BasicEvents.Start in Quickinfos und Deklarationsausgaben serialisiert würde (zumindest wenn es keinen syntaktischen Kontext gibt, um das Mitglied mit AdvEvents zu verknüpfen – Wenn Sie den Mauszeiger über den wörtlichen Ausdruck AdvEvents.Start bewegen, könnte dies plausibel zu einer Schnellinfo führen, die AdvEvents.Start lautet, aber es könnte trotzdem klarer sein, BasicEvents.Start anzuzeigen).

  • Dies wäre nur in String-Enumerationen erlaubt.

Diese möchte ich gerne anprobieren.

Zur Klarstellung: Dies ist nicht zur Umsetzung freigegeben.

Hier gibt es zwei mögliche Verhaltensweisen, und beide scheinen schlecht zu sein.

Option 1: Es ist eigentlich Zucker

Wenn Spread wirklich dasselbe bedeutet wie das Kopieren der Mitglieder der Spreaded Enum, dann wird es eine große Überraschung geben, wenn Leute versuchen, den Wert der Extending Enum so zu verwenden, als ob es der Wert der Extended Enum wäre, und es nicht funktioniert.

Option 2: Es ist eigentlich kein Zucker

Die bevorzugte Option wäre, dass Spread eher wie der Union-Type-Vorschlag von @aj-r am Anfang des Threads funktioniert. Wenn dies das Verhalten ist, das die Leute bereits wollen, dann scheinen die vorhandenen Optionen auf dem Tisch aus Gründen der Verständlichkeit unbedingt vorzuziehen. Andernfalls erstellen wir eine neue Art von String-Enumeration, die sich nicht wie jede andere String-Enumeration verhält, was seltsam ist und die "Einfachheit" des Vorschlags hier zu untergraben scheint.

Welches Verhalten wollen Menschen und warum?

Variante 1 will ich nicht, weil sie zu großen Überraschungen führt.

Ich möchte Option 2, aber ich hätte gerne genügend Syntaxunterstützung, um den erwähnten Nachteil von @aj-r zu überwinden, damit ich let e: Events = Events.Pause; aus seinem Beispiel schreiben könnte. Der Nachteil ist nicht schlimm, aber es ist ein Ort, an dem die erweiterte Aufzählung die Implementierung nicht verbergen kann; also irgendwie krass.

Variante 1 halte ich auch für eine schlechte Idee. Ich habe in meiner Firma nach Verweisen auf dieses Problem gesucht und zwei Code-Reviews gefunden, in denen es verlinkt war, und in beiden Fällen (und nach meiner persönlichen Erfahrung) besteht eine klare Notwendigkeit, dass Elemente der kleineren Aufzählung dem größeren Aufzählungstyp zugewiesen werden können . Ich mache mir besonders Sorgen darüber, dass eine Person, die ... einführt, denkt, dass es sich wie Option 2 verhält, und dann die nächste Person wirklich verwirrt wird (oder auf Hacks wie as unknown as Events.Pause zurückgreift), wenn komplexere Fälle nicht funktionieren.

Es gibt bereits viele Möglichkeiten, das Verhalten von Option 2 zu erreichen: viele Schnipsel in diesem Thread sowie verschiedene Ansätze mit String-Literal-Vereinigungen. Meine große Sorge bei der Implementierung von Option 1 ist, dass sie effektiv einen weiteren falschen Weg einführt, um Option 2 zu erhalten, und somit zu mehr Fehlerbehebung und Frustration für Leute führt, die diesen Teil von TypeScript lernen.

Welches Verhalten wollen Menschen und warum?

Da Code lauter spricht als Worte und anhand des Beispiels aus dem 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

Dieses Beispiel zeigt Option 2, richtig? Wenn ja, dann möchte ich unbedingt Option 2.

Andernfalls erstellen wir eine neue Art von String-Enumeration, die sich nicht wie jede andere String-Enumeration verhält, was seltsam ist und die "Einfachheit" des Vorschlags hier zu untergraben scheint

Ich stimme zu, dass Option 2 ein wenig beunruhigend ist und es vielleicht am besten ist, einen Schritt zurückzutreten und über Alternativen nachzudenken. Hier ist eine Untersuchung, wie dies möglich ist, ohne den heutigen Enumerationen eine neue Syntax oder ein neues Verhalten hinzuzufügen:

Ich denke, mein Vorschlag in https://github.com/microsoft/TypeScript/issues/17592#issuecomment -449440944 kommt heutzutage am nächsten und könnte etwas sein, an dem man arbeiten kann:

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

Ich sehe zwei Hauptprobleme bei diesem Ansatz:

  • Es ist wirklich verwirrend für jemanden, der TypeScript lernt; Wenn Sie den Typraum im Vergleich zum Werteraum nicht gut verstehen, sieht es so aus, als würde ein Typ mit einer Konstante überschrieben.
  • Es ist unvollständig, da es Ihnen nicht erlaubt, Events.Pause als Typ zu verwenden.

Ich habe zuvor https://github.com/microsoft/TypeScript/issues/29130 vorgeschlagen, das (unter anderem) den zweiten Aufzählungspunkt ansprechen würde, und ich denke, es könnte immer noch wertvoll sein, obwohl es sicherlich eine Menge Subtilität hinzufügen würde wie Namen funktionieren.

Eine Syntaxidee, die meiner Meinung nach beide Punkte ansprechen würde, ist eine alternative const -Syntax, die auch einen Typ deklariert:

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

Dies würde einer Welt näher kommen, in der Aufzählungen überhaupt nicht erforderlich sind. (Für mich haben sich Enumerationen immer so angefühlt, als würden sie gegen die TS-Designziele verstoßen, da sie eine Syntax auf Ausdrucksebene mit nicht trivialem Emit-Verhalten und standardmäßig nominal sind.) Die const type -Deklarationssyntax würde Sie auch lassen Erstellen Sie eine strukturell typisierte Zeichenfolgenaufzählung wie folgt:

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

Zugegebenermaßen müsste dies so funktionieren, dass der Typ BasicEvents eine Abkürzung für typeof BasicEvents[keyof typeof BasicEvents] ist, was für eine generisch benannte Syntax wie const type zu fokussiert sein könnte. Vielleicht wäre ein anderes Stichwort besser. Schade, dass const enum schon vergeben sind 😛.

Von dort aus denke ich, dass die einzige Lücke zwischen (Zeichenfolgen-) Aufzählungen und Objektliteralen die nominale Typisierung wäre. Dies könnte möglicherweise mit einer Syntax wie as unique oder const unique type gelöst werden, die im Grunde genommen ein Enum-ähnliches nominelles Typisierungsverhalten für diese Objekttypen und idealerweise auch für reguläre Zeichenfolgenkonstanten aktiviert. Das würde auch eine klare Wahl zwischen Option 1 und Option 2 bei der Definition Events geben: Sie verwenden unique , wenn Sie beabsichtigen, dass Events ein vollständig unterschiedlicher Typ ist (Option 1), und Sie lassen unique weg, wenn Sie die Zuweisung zwischen Events und BasicEvents wünschen (Option 2).

Mit const type und const unique type gäbe es eine Möglichkeit, vorhandene Aufzählungen sauber zu vereinen, und auch eine Möglichkeit, Aufzählungen als eine natürliche Kombination von TS-Funktionen und nicht als einmalig auszudrücken.

Was passiert hier? 😅

Wow von 2017 🤪, was braucht man mehr Feedback?

Was brauchst du noch für Feedback?

Genau hier??? Wir setzen Vorschläge nicht nur um, weil sie alt sind!

Was brauchst du noch für Feedback?

Genau hier??? Wir setzen Vorschläge nicht nur um, weil sie alt sind!

Jawohl. Ich war mir nicht sicher, wie es war.

Nochmals lesen und auch #40998 sehen. Ich denke, das ist der beste Weg ... die Emuns sind Objekte und ich denke, die Verbreitung ist einfacher, Enumerationen zusammenzuführen / zu erweitern.

Ich glaube nicht, dass ich qualifiziert genug bin, um meine Meinung zum Sprachdesign zu äußern, aber ich denke, dass ich als normaler Entwickler Feedback geben kann.

Ich bin vor ein paar Wochen in einem echten Projekt auf dieses Problem gestoßen, in dem ich diese Funktion verwenden wollte. Am Ende habe ich den Ansatz von @alangpierce verwendet. Leider kann ich aufgrund meiner Verantwortung gegenüber meinem Arbeitgeber den Code hier nicht teilen, aber hier sind ein paar Punkte:

  1. Das Wiederholen von Deklarationen (sowohl type als auch const für eine neue Aufzählung) war kein großes Problem und beeinträchtigte die Lesbarkeit nicht wesentlich.
  2. In meinem Fall stellte enum unterschiedliche Aktionen für einen bestimmten Algorithmus dar, und es gab unterschiedliche Aktionssätze für unterschiedliche Situationen in diesem Algorithmus. Durch die Verwendung der Typhierarchie konnte ich überprüfen, dass bestimmte Aktionen nicht zur Kompilierzeit ausgeführt werden konnten: Dies war der Sinn des Ganzen und erwies sich als sehr nützlich.
  3. Der Code, den ich geschrieben habe, war eine interne Bibliothek, und während die Unterscheidung zwischen verschiedenen Enum-Typen innerhalb der Bibliothek wichtig war, war es für externe Benutzer dieser Bibliothek egal. Mit diesem Ansatz konnte ich nur einen Typ exportieren, der der Summentyp aller verschiedenen Enumerationen darin war, und die Implementierungsdetails ausblenden.
  4. Leider war ich nicht in der Lage, eine idiomatische und einfach zu lesende Möglichkeit zu finden, Werte des Summentyps automatisch aus den Typdeklarationen zu analysieren. (In meinem Fall wurden verschiedene von mir erwähnte Algorithmusschritte zur Laufzeit aus der SQL-Datenbank geladen). Es war kein großes Problem (das manuelle Schreiben des Parsing-Codes war einfach genug), aber es wäre schön, wenn die Implementierung der Enum-Vererbung diesem Problem etwas Aufmerksamkeit schenken würde.

Insgesamt denke ich, dass viele echte Projekte mit langweiliger Geschäftslogik sehr von diesem Feature profitieren würden. Das Aufteilen von Aufzählungen in verschiedene Untertypen würde es dem Typsystem ermöglichen, viele Invarianten zu überprüfen, die jetzt durch Komponententests überprüft werden, und es ist immer eine gute Sache, falsche Werte durch ein Typsystem nicht darstellbar zu machen.

Hallo,

Lassen Sie mich hier meinen Senf hinzufügen 🙂

Mein Kontext

Ich habe eine API mit der mit tsoa generierten OpenApi-Dokumentation.

Eines meiner Modelle hat einen Status, der wie folgt definiert ist:

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

Ich habe eine Methode setStatus , die eine Teilmenge dieser Status annimmt. Da die Funktion nicht verfügbar ist, habe ich überlegt, die Aufzählung auf diese Weise zu duplizieren:

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

Also meine Methode ist so beschrieben:

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

mit diesem Code bekomme ich diesen Fehler:

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.

was ich vorerst tun werde, war aber neugierig und fing an, dieses Repository zu durchsuchen, als ich das fand.
Nachdem ich ganz nach unten gescrollt habe, habe ich niemanden gefunden (oder vielleicht habe ich etwas übersehen), der diesen Anwendungsfall vorschlägt.

In meinem Anwendungsfall möchte ich keine Aufzählung "erweitern", da es keinen Grund dafür gibt, dass EntityStatus eine Erweiterung von RequestedEntityStatus wäre. Ich würde es vorziehen, aus der allgemeineren Aufzählung "auswählen" zu können.

Mein Vorschlag

Ich fand den Spread-Operator besser als den Extends-Vorschlag, aber ich möchte noch weiter gehen und diese vorschlagen:

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.
    }
}

Allgemeiner sollte dies funktionieren:

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

Mit anderen Worten

Ich möchte die Einschränkungen für Enum-Typen lockern, um sich eher wie Unions ( 'a' | 'b' ) als undurchsichtige Strukturen zu verhalten.

Durch Hinzufügen dieser Fähigkeiten zum Compiler können zwei unabhängige Aufzählungen mit denselben Werten einander mit denselben Regeln wie Vereinigungen zugewiesen werden:
Angesichts der folgenden Aufzählungen:

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

Und drei Variablen a:A , b:B und c:C

  • c = a sollte funktionieren, da A nur eine Teilmenge von C ist, also ist jeder Wert von A ein gültiger Wert von C
  • c = b sollte funktionieren, da B nur 'a' | 'c' ist, was beides gültige Werte von C sind
  • b = a könnte funktionieren, wenn bekannt ist, dass sich a von 'b' unterscheidet (was nur dem Typ 'a' entsprechen würde)
  • a = b könnte ebenfalls funktionieren, wenn bekannt ist, dass sich b von 'c' unterscheidet
  • b = c könnte funktionieren, wenn bekannt ist, dass sich c von 'b' unterscheidet (was 'a'|'c' entsprechen würde, was genau B ist).

oder sollten wir vielleicht eine explizite Besetzung auf der rechten Seite benötigen, wie für den Gleichheitsvergleich?

Über Aufzählungsmitgliederkonflikte

Ich bin kein Fan der „Last Wins“-Regel, auch wenn es sich beim Spread-Operator natürlich anfühlt.
Ich würde sagen, dass der Compiler einen Fehler zurückgeben sollte, wenn entweder ein "Schlüssel" oder ein "Wert" der Aufzählung dupliziert wird, es sei denn, Schlüssel und Wert sind identisch:

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'

Schließen

Ich finde diesen Vorschlag ziemlich flexibel und natürlich für TS-Entwickler, mit dem er arbeiten kann, während er mehr Typsicherheit (im Vergleich zu as unknown as T ) ermöglicht, ohne tatsächlich zu ändern, was eine Aufzählung ist. Es führt lediglich eine neue Möglichkeit ein, Mitglieder zu Aufzählungen hinzuzufügen, und eine neue Möglichkeit, Aufzählungen miteinander zu vergleichen.
Was denken Sie? Habe ich ein offensichtliches Problem mit der Spracharchitektur übersehen, das diese Funktion unerreichbar macht?

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen