Typoskript hat Gewerkschaften, sind also Aufzählungen überflüssig?

77

Seit TypeScript Gewerkschaftstypen eingeführt hat, frage ich mich, ob es einen Grund gibt, einen Aufzählungstyp zu deklarieren. Betrachten Sie die folgende Aufzählungstypdeklaration:

enum X { A, B, C }
var x:X = X.A;

und eine ähnliche Erklärung zum Unionstyp:

type X: "A" | "B" | "C"
var x:X = "A";

Wenn sie im Grunde dem gleichen Zweck dienen und die Gewerkschaften mächtiger und ausdrucksvoller sind, warum sind dann Aufzählungen notwendig?

prmph
quelle
1
Aufzählungen werden Zahlen zugeordnet, was in bestimmten Situationen hilfreich sein kann. Ich gehe davon aus, dass sie irgendwann auch mehr mit Aufzählungen tun möchten, z. B. ihnen Eigenschaften geben, die Sie aufrufen können (wie C # - oder Java-Aufzählungen).
PaulBGD

Antworten:

62

Soweit ich sehe, sind sie nicht redundant, da Unionstypen nur ein Konzept zur Kompilierungszeit sind, während Aufzählungen tatsächlich transpiliert werden und im resultierenden Javascript ( Beispiel ) landen .

Auf diese Weise können Sie einige Dinge mit Aufzählungen tun, die mit Vereinigungstypen ansonsten nicht möglich sind (z. B. Aufzählung der möglichen Aufzählungswerte ).

Inmitten
quelle
1
Ich denke, Sie wollen damit sagen, dass es Gründe gibt, eine zu verwenden enum. Der erste Satz ist verwirrend. "Soweit ich sehe, sind [Aufzählungen] nicht ..." Beantwortung der Fragen "gibt es [einen] Grund, einen Aufzählungstyp zu deklarieren."
Matthew
1
In meiner Lektüre bezog sich der erste Satz "Soweit ich sehe, dass dies nicht der Fall ist" auf Aufzählungen in der Titelfrage "... sind Aufzählungen also überflüssig?" . Ich
reiche
40

Mit den neuesten Versionen von TypeScript ist es einfach, iterierbare Vereinigungstypen zu deklarieren. Daher sollten Sie Unionstypen Enums vorziehen.

So deklarieren Sie iterierbare Vereinigungstypen

const permissions = ['read', 'write', 'execute'] as const;
type Permission = typeof permissions[number]; // 'read' | 'write' | 'execute'

// you can iterate over permissions
for (const permission of permissions) {
  // do something
}

Wenn sich die tatsächlichen Werte des Unionstyps nicht sehr gut beschreiben, können Sie sie wie bei Aufzählungen benennen.

// when you use enum
enum Permission {
  Read = 'r',
  Write = 'w',
  Execute = 'x'
}

// union type equivalent
const Permission = {
  Read: 'r',
  Write: 'w',
  Execute: 'x'
} as const;
type Permission = typeof Permission[keyof typeof Permission]; // 'r' | 'w' | 'x'

// of course it's quite easy to iterate over
for (const permission of Object.values(Permission)) {
  // do something
}

Verpassen Sie nicht die as constBehauptung, die in diesen Mustern die entscheidende Rolle spielt.

Warum ist es nicht gut, Aufzählungen zu verwenden?

1. Nicht konstante Aufzählungen passen nicht zum Konzept "eine typisierte Obermenge von JavaScript".

Ich denke, dieses Konzept ist einer der entscheidenden Gründe, warum TypeScript unter anderen altJS-Sprachen so beliebt geworden ist. Nicht konstante Enums verletzen das Konzept, indem sie zur Laufzeit lebende JavaScript-Objekte mit einer Syntax ausgeben, die nicht mit JavaScript kompatibel ist.

2. Konstanten haben einige Fallstricke

Konstanten können mit Babel nicht transpiliert werden

Derzeit gibt es zwei Problemumgehungen für dieses Problem: Sie müssen Konstanten manuell oder mit dem Plugin entfernen babel-plugin-const-enum.

Das Deklarieren von Konstanten in einem Umgebungskontext kann problematisch sein

Umgebungskonstanten sind nicht zulässig, wenn das --isolatedModulesFlag bereitgestellt wird. Ein TypeScript-Teammitglied sagt, dass " const enumin DT wirklich keinen Sinn ergibt" (DT bezieht sich auf DefinitelyTyped) und "Sie sollten stattdessen einen Union-Typ von Literalen (Zeichenfolge oder Zahl) verwenden" anstelle von Konstanten im Umgebungskontext.

Konstanten unter --isolatedModulesFlagge verhalten sich auch außerhalb eines Umgebungskontexts seltsam

Ich war überrascht, diesen Kommentar auf GitHub zu lesen und bestätigte, dass das Verhalten mit TypeScript 3.8.2 immer noch stimmt.

3. Numerische Aufzählungen sind nicht typsicher

Sie können numerischen Aufzählungen eine beliebige Nummer zuweisen.

enum ZeroOrOne {
  Zero = 0,
  One = 1
}
const zeroOrOne: ZeroOrOne = 2; // no error!!

4. Die Deklaration von String-Aufzählungen kann redundant sein

Wir sehen manchmal diese Art von String-Aufzählungen:

enum Day {
  Sunday = 'Sunday',
  Monday = 'Monday',
  Tuesday = 'Tuesday',
  Wednesday = 'Wednesday',
  Thursday = 'Thursday',
  Friday = 'Friday',
  Saturday = 'Saturday'
}

5. Das Muster der Vereinigungstypen ist viel cooler

Die Muster, die ich am Anfang dieser Antwort gezeigt habe, sind die großartigen Beispiele aus der Praxis, wie flexibel und ausdrucksstark TypeScript ist. Ich glaube, Sie können TypeScript besser verstehen und ein guter TypeScript-Programmierer sein, wenn Sie solche Muster aggressiv anwenden.

Ich muss zugeben, dass es eine Aufzählungsfunktion gibt, die von Gewerkschaftstypen nicht erreicht wird

Auch wenn aus dem Kontext ersichtlich ist, dass der Zeichenfolgenwert in der Aufzählung enthalten ist, können Sie ihn nicht der Aufzählung zuweisen.

enum StringEnum {
  Foo = 'foo'
}
const foo1: StringEnum = StringEnum.Foo; // no error
const foo2: StringEnum = 'foo'; // error!!

Dies vereinheitlicht den Stil der Enum-Wertzuweisung im gesamten Code, indem die Verwendung von Zeichenfolgenwerten oder Zeichenfolgenliteralen vermieden wird. Dieses Verhalten stimmt nicht mit dem Verhalten des TypeScript-Typsystems an den anderen Stellen überein und ist etwas überraschend. Einige Leute, die dachten, dies sollte behoben werden, werfen Probleme auf ( dies und das ), in denen wiederholt erwähnt wird, dass die Absicht von String-Aufzählungen besteht um "undurchsichtige" Zeichenfolgentypen bereitzustellen: dh sie können geändert werden, ohne die Verbraucher zu ändern.

enum Weekend {
  Saturday = 'Saturday',
  Sunday = 'Sunday'
}
// As this style is forced, you can change the value of
// Weekend.Saturday to 'Sat' without modifying consumers
const weekend: Weekend = Weekend.Saturday;

Beachten Sie, dass diese "Undurchsichtigkeit" nicht perfekt ist, da die Zuweisung von Aufzählungswerten zu Zeichenfolgenliteraltypen nicht beschränkt ist.

enum Weekend {
  Saturday = 'Saturday',
  Sunday = 'Sunday'
}
// The change of the value of Weekend.Saturday to 'Sat'
// results in a compilation error
const saturday: 'Saturday' = Weekend.Saturday;

Wenn Sie der Meinung sind, dass diese "undurchsichtige" Funktion so wertvoll ist, dass Sie alle oben beschriebenen Nachteile im Austausch dafür akzeptieren können, können Sie Zeichenfolgenaufzählungen nicht aufgeben.

So entfernen Sie Aufzählungen aus Ihrer Codebasis

Mit der no-restricted-syntaxRegel von ESLint, wie beschrieben .

Kimamula
quelle
OK Cool. Aber was ist, wenn Sie diese Deklaration in eine gemeinsam genutzte d.ts-Typisierungsdatei einfügen möchten? Sie können eine
Konstante nur
1
Ich denke nicht, dass es möglich ist, über etwas zu iterieren, das nur in einem Umgebungskontext deklariert ist. Das Bereitstellen sowohl der Deklaration declare const permissions: readonly ['read', 'write', 'execute']als auch des realen JavaScript-Objekts const permissions = ['read', 'write', 'execute']sollte funktionieren.
Kimamula
1
Ich möchte nicht, dass String-Literale zuweisbar oder mit Enums vergleichbar sind. const foo: StringEnum === StringEnum.Foo // true or false Ich möchte, dass diese Einschränkung sicherstellt, dass wir keine Verwechslungen von String-Literalen haben.
Matthew
@matthew Danke für deinen Kommentar. Ich habe meine Antwort aktualisiert, um fair zu sein.
Kimamula
Tolles Schreiben! Es kann sich jedoch lohnen, einen Nachteil von Zeichenfolgenvereinigungstypen hinzuzufügen, der mit Ihrem Punkt zur Undurchsichtigkeit zusammenhängt: TypeScript unterscheidet nicht zwischen einer konstanten Zeichenfolge wie "Lesen", die in verschiedenen Vereinigungstypen verwendet wird. Wenn Sie also nach Referenzen für diese Zeichenfolge suchen, werden die Referenzen für alle Vereinigungstypen angezeigt, in denen sie verwendet wird. Dies ist der Hauptgrund, warum das "Symbol umbenennen" auch für diese Zeichenfolgen nicht ausgeführt werden kann.
Rene Hamburger
29

Es gibt nur wenige Gründe, warum Sie eine verwenden möchten enum

Ich sehe die großen Vorteile der Verwendung einer Union darin, dass sie eine prägnante Möglichkeit bieten, einen Wert mit mehreren Typen darzustellen, und dass sie sehr gut lesbar sind. let x: number | string

BEARBEITEN: Ab TypeScript 2.4 unterstützen Enums jetzt Zeichenfolgen.

enum Colors {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
} 
Matthew
quelle
4
Bin ich der einzige, der denkt, dass die Verwendung enumvon "Bit-Flags" ein Anti-Pattern ist?
Cameron Tacklind
14

Aufzählungen können konzeptionell als Teilmenge von Vereinigungstypen betrachtet werden, die bestimmten Werten intund / oder stringWerten gewidmet sind, wobei einige zusätzliche Funktionen in anderen Antworten erwähnt werden, die sie benutzerfreundlich machen, z . B. Namespace .

In Bezug auf die Typensicherheit sind numerische Aufzählungen weniger sicher, dann kommen Vereinigungstypen und schließlich Zeichenfolgenaufzählungen:

// Numeric enum
enum Colors { Red, Green, Blue }
const c: Colors = 100; // ⚠️ No errors!

// Equivalent union types
type Color =
    | 0 | 'Red'
    | 1 | 'Green'
    | 2 | 'Blue';

let color: Color = 'Red'; // ✔️ No error because namespace free
color = 100; // ✔️ Error: Type '100' is not assignable to type 'Color'

type AltColor = 'Red' | 'Yellow' | 'Blue';

let altColor: AltColor = 'Red';
color = altColor; // ⚠️ No error because `altColor` type is here narrowed to `"Red"`

// String enum
enum NamedColors {
  Red   = 'Red',
  Green = 'Green',
  Blue  = 'Blue',
}

let namedColor: NamedColors = 'Red'; // ✔️ Error: Type '"Red"' is not assignable to type 'Colors'.

enum AltNamedColors {
  Red    = 'Red',
  Yellow = 'Yellow',
  Blue   = 'Blue',
}
namedColor = AltNamedColors.Red; // ✔️ Error: Type 'AltNamedColors.Red' is not assignable to type 'Colors'.

Mehr zu diesem Thema in diesem 2ality-Artikel: TypeScript-Enums: Wie funktionieren sie? Wofür können sie verwendet werden?


Unionstypen unterstützen heterogene Daten und Strukturen und ermöglichen beispielsweise Polymorphismus:

class RGB {
    constructor(
        readonly r: number,
        readonly g: number,
        readonly b: number) { }

    toHSL() {
        return new HSL(0, 0, 0); // Fake formula
    }
}

class HSL {
    constructor(
        readonly h: number,
        readonly s: number,
        readonly l: number) { }

    lighten() {
        return new HSL(this.h, this.s, this.l + 10);
    }
}

function lightenColor(c: RGB | HSL) {
    return (c instanceof RGB ? c.toHSL() : c).lighten();
}

Zwischen Aufzählungen und Vereinigungstypen können Singletons Aufzählungen ersetzen. Es ist ausführlicher, aber auch objektorientierter :

class Color {
    static readonly Red   = new Color(1, 'Red',   '#FF0000');
    static readonly Green = new Color(2, 'Green', '#00FF00');
    static readonly Blue  = new Color(3, 'Blue',  '#0000FF');

    static readonly All: readonly Color[] = [
        Color.Red,
        Color.Green,
        Color.Blue,
    ];

    private constructor(
        readonly id: number,
        readonly label: string,
        readonly hex: string) { }
}

const c = Color.Red;

const colorIds = Color.All.map(x => x.id);

Ich neige dazu, F # zu betrachten, um gute Modellierungspraktiken zu sehen. Ein Zitat aus einem Artikel über F # -Aufzählungen auf F # für Spaß und Gewinn , das hier nützlich sein kann:

Im Allgemeinen sollten Sie diskriminierte Vereinigungstypen Enums vorziehen, es sei denn, Sie müssen wirklich einen int (oder einen string) Wert damit verknüpfen

Es gibt andere Alternativen zu Modellaufzählungen. Einige von ihnen werden in diesem anderen 2ality-Artikel ausführlich beschrieben. Alternativen zu Aufzählungen in TypeScript .

Romain Deneau
quelle
0

Der Aufzählungstyp ist nicht redundant, aber in den meisten Fällen wird die Vereinigung bevorzugt.

Aber nicht immer. Die Verwendung von Aufzählungen zur Darstellung von z. B. Zustandsübergängen könnte viel praktischer und aussagekräftiger sein als die Verwendung von union **

Betrachten Sie ein reales Live-Szenario:

enum OperationStatus {
  NEW = 1,
  PROCESSING = 2,
  COMPLETED = 4
}

OperationStatus.PROCESSING > OperationStatus.NEW // true
OperationStatus.PROCESSING > OperationStatus.COMPLETED // false
John Smith
quelle
2
Ich denke, wenn Sie dies tun, sollten Sie die Zahlenwerte in der Enum-Deklaration explizit zuweisen, um zu verhindern, dass eine Neuordnung (z. B. weil jemand anderes es für sinnvoller hält, sie alphabetisch zu haben) einen Laufzeitfehler verursacht, der möglicherweise übersehen wird.
dpoetzsch