TypeScript meistern
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

12 TypeScript Enums: Wie funktionieren sie? Wofür können sie verwendet werden?



Dieses Kapitel beantwortet die folgenden beiden Fragen

Im nächsten Kapitel betrachten wir Alternativen zu Enums.

12.1 Die Grundlagen

boolean ist ein Typ mit einer endlichen Anzahl von Werten: false und true. Mit Enums können wir ähnliche Typen selbst definieren.

12.1.1 Numerische Enums

Dies ist ein numerisches Enum

enum NoYes {
  No = 0,
  Yes = 1, // trailing comma
}

assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);

Erläuterungen

Wir können Mitglieder wie Literale verwenden, z. B. true, 123 oder 'abc' – zum Beispiel

function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}
assert.equal(toGerman(NoYes.No), 'Nein');
assert.equal(toGerman(NoYes.Yes), 'Ja');

12.1.2 Auf Zeichenketten basierende Enums

Anstelle von Zahlen können wir auch Zeichenketten als Enum-Mitgliedswerte verwenden

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');

12.1.3 Heterogene Enums

Die letzte Art von Enums wird als heterogen bezeichnet. Die Mitgliedswerte eines heterogenen Enums sind eine Mischung aus Zahlen und Zeichenketten

enum Enum {
  One = 'One',
  Two = 'Two',
  Three = 3,
  Four = 4,
}
assert.deepEqual(
  [Enum.One, Enum.Two, Enum.Three, Enum.Four],
  ['One', 'Two', 3, 4]
);

Heterogene Enums werden nicht oft verwendet, da sie wenige Anwendungsfälle haben.

Leider unterstützt TypeScript nur Zahlen und Zeichenketten als Enum-Mitgliedswerte. Andere Werte, wie Symbole, sind nicht erlaubt.

12.1.4 Weglassen von Initialisierern

Wir können Initialisierer in zwei Fällen weglassen

Dies ist ein numerisches Enum ohne Initialisierer

enum NoYes {
  No,
  Yes,
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);

Dies ist ein heterogenes Enum, bei dem einige Initialisierer weggelassen wurden

enum Enum {
  A,
  B,
  C = 'C',
  D = 'D',
  E = 8, // (A)
  F,
}
assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0, 1, 'C', 'D', 8, 9]
);

Beachten Sie, dass wir den Initialisierer in Zeile A nicht weglassen können, da der Wert des vorhergehenden Mitglieds keine Zahl ist.

12.1.5 Groß- und Kleinschreibung von Enum-Mitgliedsnamen

Es gibt mehrere Präzedenzfälle für die Benennung von Konstanten (in Enums oder anderswo)

12.1.6 Anführungszeichen für Enum-Mitgliedsnamen

Ähnlich wie bei JavaScript-Objekten können wir die Namen von Enum-Mitgliedern in Anführungszeichen setzen

enum HttpRequestField {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}
assert.equal(HttpRequestField['Accept-Charset'], 1);

Es gibt keine Möglichkeit, die Namen von Enum-Mitgliedern zu berechnen. Objekt-Literale unterstützen berechnete Schlüsseleigenschaften über eckige Klammern.

12.2 Festlegen von Enum-Mitgliedswerten (Fortgeschritten)

TypeScript unterscheidet drei Arten von Enum-Mitgliedern, je nachdem, wie sie initialisiert werden

Bisher haben wir nur Literal-Mitglieder verwendet.

In der vorherigen Liste sind die früher erwähnten Mitglieder weniger flexibel, unterstützen aber mehr Funktionen. Lesen Sie weiter für weitere Informationen.

12.2.1 Literal-Enum-Mitglieder

Ein Enum-Mitglied ist literal, wenn sein Wert angegeben ist

Wenn ein Enum nur Literal-Mitglieder hat, können wir diese Mitglieder als Typen verwenden (ähnlich wie z. B. Zahlen-Literale als Typen verwendet werden können)

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(x: NoYes.No) { // (A)
  return x;
}

func(NoYes.No); // OK

// @ts-expect-error: Argument of type '"No"' is not assignable to
// parameter of type 'NoYes.No'.
func('No');

// @ts-expect-error: Argument of type 'NoYes.Yes' is not assignable to
// parameter of type 'NoYes.No'.
func(NoYes.Yes);

NoYes.No in Zeile A ist ein Enum-Mitgliedstyp.

Zusätzlich unterstützen Literal-Enums Erschöpfungsprüfungen (die wir später noch betrachten werden).

12.2.2 Konstante Enum-Mitglieder

Ein Enum-Mitglied ist konstant, wenn sein Wert zur Kompilierzeit berechnet werden kann. Daher können wir seinen Wert entweder implizit angeben (d. h. wir lassen TypeScript ihn für uns angeben). Oder wir geben ihn explizit an und dürfen nur die folgende Syntax verwenden

Dies ist ein Beispiel für ein Enum, dessen Mitglieder alle konstant sind (wir werden später sehen, wie dieses Enum verwendet wird)

enum Perm {
  UserRead     = 1 << 8, // bit 8
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}

Im Allgemeinen können konstante Mitglieder nicht als Typen verwendet werden. Erschöpfungsprüfungen werden jedoch weiterhin durchgeführt.

12.2.3 Berechnete Enum-Mitglieder

Die Werte von berechneten Enum-Mitgliedern können über beliebige Ausdrücke angegeben werden. Zum Beispiel

enum NoYesNum {
  No = 123,
  Yes = Math.random(), // OK
}

Dies war ein numerisches Enum. Auf Zeichenketten basierende Enums und heterogene Enums sind begrenzter. Zum Beispiel können wir keine Methodenaufrufe verwenden, um Mitgliedswerte anzugeben

enum NoYesStr {
  No = 'No',
  // @ts-expect-error: Computed values are not permitted in
  // an enum with string valued members.
  Yes = ['Y', 'e', 's'].join(''),
}

TypeScript führt keine Erschöpfungsprüfungen für berechnete Enum-Mitglieder durch.

12.3 Nachteile von numerischen Enums

12.3.1 Nachteil: Protokollierung

Beim Protokollieren von Mitgliedern numerischer Enums sehen wir nur Zahlen

enum NoYes { No, Yes }

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 0
// 1

12.3.2 Nachteil: lose Typüberprüfung

Wenn das Enum als Typ verwendet wird, sind die statisch erlaubten Werte nicht nur die der Enum-Mitglieder – jede beliebige Zahl wird akzeptiert

enum NoYes { No, Yes }
function func(noYes: NoYes) {}
func(33); // no error!

Warum gibt es keine strengeren statischen Prüfungen? Daniel Rosenwasser erklärt

Das Verhalten wird durch bitweise Operationen motiviert. Es gibt Zeiten, in denen SomeFlag.Foo | SomeFlag.Bar dazu bestimmt ist, ein anderes SomeFlag zu ergeben. Stattdessen erhalten Sie number, und Sie möchten nicht jedes Mal zurück zu SomeFlag casten müssen.

Ich glaube, wenn wir TypeScript noch einmal machen und immer noch Enums hätten, hätten wir ein separates Konstrukt für Bit-Flags gemacht.

Wie Enums für Bitmuster verwendet werden, wird bald detaillierter gezeigt.

12.3.3 Empfehlung: Bevorzugen Sie auf Zeichenketten basierende Enums

Meine Empfehlung ist, auf Zeichenketten basierende Enums zu bevorzugen (der Kürze halber folgt dieses Kapitel nicht immer dieser Empfehlung)

enum NoYes { No='No', Yes='Yes' }

Einerseits ist die Protokollausgabe für Menschen nützlicher

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 'No'
// 'Yes'

Andererseits erhalten wir eine strengere Typüberprüfung

function func(noYes: NoYes) {}

// @ts-expect-error: Argument of type '"abc"' is not assignable
// to parameter of type 'NoYes'.
func('abc');

// @ts-expect-error: Argument of type '"Yes"' is not assignable
// to parameter of type 'NoYes'.
func('Yes'); // (A)

Nicht einmal Zeichenketten, die gleich den Werten der Mitglieder sind, sind erlaubt (Zeile A).

12.4 Anwendungsfälle für Enums

12.4.1 Anwendungsfall: Bitmuster

Im Node.js-Dateisystemmodul haben mehrere Funktionen den Parameter mode. Er gibt Dateiberechtigungen über eine numerische Kodierung an, die ein Überbleibsel von Unix ist

Das bedeutet, dass Berechtigungen durch 9 Bits dargestellt werden können (3 Kategorien mit jeweils 3 Berechtigungen)

Benutzer Gruppe Alle
Berechtigungen r, w, x r, w, x r, w, x
Bit 8, 7, 6 5, 4, 3 2, 1, 0

Node.js macht das nicht, aber wir könnten ein Enum verwenden, um mit diesen Flags zu arbeiten

enum Perm {
  UserRead     = 1 << 8, // bit 8
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}

Bitmuster werden über Bitweises ODER kombiniert

// User can change, read and execute.
// Everyone else can only read and execute.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.UserExecute |
  Perm.GroupRead | Perm.GroupExecute |
  Perm.AllRead | Perm.AllExecute,
  0o755);

// User can read and write.
// Group members can read.
// Everyone can’t access at all.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.GroupRead,
  0o640);
12.4.1.1 Eine Alternative zu Bitmustern

Die Grundidee hinter Bitmustern ist, dass es eine Menge von Flags gibt und jede Teilmenge dieser Flags ausgewählt werden kann.

Daher ist die Verwendung echter Mengen zur Auswahl von Teilmengen ein direkterer Weg, um die gleiche Aufgabe auszuführen

enum Perm {
  UserRead = 'UserRead',
  UserWrite = 'UserWrite',
  UserExecute = 'UserExecute',
  GroupRead = 'GroupRead',
  GroupWrite = 'GroupWrite',
  GroupExecute = 'GroupExecute',
  AllRead = 'AllRead',
  AllWrite = 'AllWrite',
  AllExecute = 'AllExecute',
}
function writeFileSync(
  thePath: string, permissions: Set<Perm>, content: string) {
  // ···
}
writeFileSync(
  '/tmp/hello.txt',
  new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
  'Hello!');

12.4.2 Anwendungsfall: Mehrere Konstanten

Manchmal haben wir Sätze von Konstanten, die zusammengehören

const off = Symbol('off');
const info = Symbol('info');
const warn = Symbol('warn');
const error = Symbol('error');

Dies ist ein guter Anwendungsfall für ein Enum

enum LogLevel {
  off = 'off',
  info = 'info',
  warn = 'warn',
  error = 'error',
}

Ein Vorteil des Enums ist, dass die Konstantenamen gruppiert und in den Namespace LogLevel verschachtelt sind.

Ein weiterer Vorteil ist, dass wir automatisch den Typ LogLevel dafür erhalten. Wenn wir einen solchen Typ für die Konstanten wollen, brauchen wir mehr Aufwand

type LogLevel =
  | typeof off
  | typeof info
  | typeof warn
  | typeof error
;

Weitere Informationen zu diesem Ansatz finden Sie in §13.1.3 „Unionen von Symbol-Singleton-Typen“.

12.4.3 Anwendungsfall: Aussagekräftiger als Booleans

Wenn Booleans verwendet werden, um Alternativen darzustellen, sind Enums normalerweise aussagekräftiger.

12.4.3.1 Boolean-ähnliches Beispiel: geordnete vs. ungeordnete Listen

Um beispielsweise darzustellen, ob eine Liste geordnet ist oder nicht, können wir einen Boolean verwenden

class List1 {
  isOrdered: boolean;
  // ···
}

Ein Enum ist jedoch aussagekräftiger und hat den zusätzlichen Vorteil, dass wir später weitere Alternativen hinzufügen können, wenn wir sie brauchen.

enum ListKind { ordered, unordered }
class List2 {
  listKind: ListKind;
  // ···
}
12.4.3.2 Boolean-ähnliches Beispiel: Fehlerbehandlungsmodi

Ähnlich können wir angeben, wie Fehler behandelt werden sollen, über einen Boolean-Wert

function convertToHtml1(markdown: string, throwOnError: boolean) {
  // ···
}

Oder wir können dies über einen Enum-Wert tun

enum ErrorHandling {
  throwOnError = 'throwOnError',
  showErrorsInContent = 'showErrorsInContent',
}
function convertToHtml2(markdown: string, errorHandling: ErrorHandling) {
  // ···
}

12.4.4 Anwendungsfall: Bessere Zeichenketten-Konstanten

Betrachten Sie die folgende Funktion, die reguläre Ausdrücke erstellt.

const GLOBAL = 'g';
const NOT_GLOBAL = '';
type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;

function createRegExp(source: string,
  globalness: Globalness = NOT_GLOBAL) {
    return new RegExp(source, 'u' + globalness);
  }

assert.deepEqual(
  createRegExp('abc', GLOBAL),
  /abc/ug);

assert.deepEqual(
  createRegExp('abc', 'g'), // OK
  /abc/ug);

Anstelle der Zeichenketten-Konstanten können wir ein Enum verwenden

enum Globalness {
  Global = 'g',
  notGlobal = '',
}

function createRegExp(source: string, globalness = Globalness.notGlobal) {
  return new RegExp(source, 'u' + globalness);
}

assert.deepEqual(
  createRegExp('abc', Globalness.Global),
  /abc/ug);

assert.deepEqual(
  // @ts-expect-error: Argument of type '"g"' is not assignable to parameter of type 'Globalness | undefined'. (2345)
  createRegExp('abc', 'g'), // error
  /abc/ug);

Was sind die Vorteile dieses Ansatzes?

12.5 Enums zur Laufzeit

TypeScript kompiliert Enums in JavaScript-Objekte. Als Beispiel nehmen wir das folgende Enum

enum NoYes {
  No,
  Yes,
}

TypeScript kompiliert dieses Enum zu

var NoYes;
(function (NoYes) {
  NoYes[NoYes["No"] = 0] = "No";
  NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));

In diesem Code werden die folgenden Zuweisungen vorgenommen

NoYes["No"] = 0;
NoYes["Yes"] = 1;

NoYes[0] = "No";
NoYes[1] = "Yes";

Es gibt zwei Gruppen von Zuweisungen

12.5.1 Umgekehrte Abbildungen

Gegeben sei ein numerisches Enum

enum NoYes {
  No,
  Yes,
}

Die normale Abbildung ist von Mitgliedsnamen zu Mitgliedswerten

// Static (= fixed) lookup:
assert.equal(NoYes.Yes, 1);

// Dynamic lookup:
assert.equal(NoYes['Yes'], 1);

Numerische Enums unterstützen auch eine umgekehrte Abbildung von Mitgliedswerten zu Mitgliedsnamen

assert.equal(NoYes[1], 'Yes');

Ein Anwendungsfall für umgekehrte Abbildungen ist das Drucken des Namens eines Enum-Mitglieds

function getQualifiedName(value: NoYes) {
  return 'NoYes.' + NoYes[value];
}
assert.equal(
  getQualifiedName(NoYes.Yes), 'NoYes.Yes');

12.5.2 Auf Zeichenketten basierende Enums zur Laufzeit

Auf Zeichenketten basierende Enums haben eine einfachere Darstellung zur Laufzeit.

Betrachten Sie das folgende Enum.

enum NoYes {
  No = 'NO!',
  Yes = 'YES!',
}

Es wird zu diesem JavaScript-Code kompiliert

var NoYes;
(function (NoYes) {
    NoYes["No"] = "NO!";
    NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));

TypeScript unterstützt keine umgekehrten Abbildungen für auf Zeichenketten basierende Enums.

12.6 const Enums

Wenn ein Enum mit dem Schlüsselwort const präfigiert ist, hat es keine Darstellung zur Laufzeit. Stattdessen werden die Werte seiner Mitglieder direkt verwendet.

12.6.1 Kompilieren von Nicht-const-Enums

Um diesen Effekt zu beobachten, betrachten wir zunächst das folgende Nicht-const-Enum

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

TypeScript kompiliert diesen Code zu

"use strict";
var NoYes;
(function (NoYes) {
  NoYes["No"] = "No";
  NoYes["Yes"] = "Yes";
})(NoYes || (NoYes = {}));

function toGerman(value) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

12.6.2 Kompilieren von const-Enums

Dies ist derselbe Code wie zuvor, aber jetzt ist das Enum const

const enum NoYes {
  No,
  Yes,
}
function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

Jetzt verschwindet die Darstellung des Enums als Konstrukt und nur die Werte seiner Mitglieder bleiben erhalten

function toGerman(value) {
  switch (value) {
    case "No" /* No */:
      return 'Nein';
    case "Yes" /* Yes */:
      return 'Ja';
  }
}

12.7 Enums zur Kompilierzeit

12.7.1 Enums sind Objekte

TypeScript behandelt (Nicht-const-)Enums so, als wären sie Objekte

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(obj: { No: string }) {
  return obj.No;
}
assert.equal(
  func(NoYes), // allowed statically!
  'No');

12.7.2 Sicherheitsprüfungen für Literal-Enums

Wenn wir einen Enum-Mitgliedswert akzeptieren, möchten wir oft sicherstellen, dass

Lesen Sie weiter für weitere Informationen. Wir werden mit dem folgenden Enum arbeiten

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
12.7.2.1 Schutz vor ungültigen Werten

Im folgenden Code treffen wir zwei Maßnahmen gegen ungültige Werte

function toGerman1(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
    default:
      throw new TypeError('Unsupported value: ' + JSON.stringify(value));
  }
}

assert.throws(
  // @ts-expect-error: Argument of type '"Maybe"' is not assignable to
  // parameter of type 'NoYes'.
  () => toGerman1('Maybe'),
  /^TypeError: Unsupported value: "Maybe"$/);

Die Maßnahmen sind

12.7.2.2 Schutz vor dem Vergessen von Fällen durch Erschöpfungsprüfungen

Wir können eine weitere Maßnahme ergreifen. Der folgende Code führt eine Erschöpfungsprüfung durch: TypeScript warnt uns, wenn wir vergessen, alle Enum-Mitglieder zu berücksichtigen.

class UnsupportedValueError extends Error {
  constructor(value: never) {
    super('Unsupported value: ' + value);
  }
}

function toGerman2(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
    default:
      throw new UnsupportedValueError(value);
  }
}

Wie funktioniert die Erschöpfungsprüfung? Für jeden Fall leitet TypeScript den Typ von value ab

function toGerman2b(value: NoYes) {
  switch (value) {
    case NoYes.No:
      // %inferred-type: NoYes.No
      value;
      return 'Nein';
    case NoYes.Yes:
      // %inferred-type: NoYes.Yes
      value;
      return 'Ja';
    default:
      // %inferred-type: never
      value;
      throw new UnsupportedValueError(value);
  }
}

Im default-Fall leitet TypeScript den Typ never für value ab, da wir dort nie ankommen. Wenn wir jedoch ein Mitglied .Maybe zu NoYes hinzufügen, ist der abgeleitete Typ von value NoYes.Maybe. Und dieser Typ ist statisch inkompatibel mit dem Typ never des Parameters von new UnsupportedValueError(). Deshalb erhalten wir die folgende Fehlermeldung zur Kompilierzeit

Argument of type 'NoYes.Maybe' is not assignable to parameter of type 'never'.

Praktischerweise funktioniert diese Art von Erschöpfungsprüfung auch mit if-Anweisungen

function toGerman3(value: NoYes) {
  if (value === NoYes.No) {
    return 'Nein';
  } else if (value === NoYes.Yes) {
    return 'Ja';
  } else {
    throw new UnsupportedValueError(value);
  }
}
12.7.2.3 Eine alternative Methode zur Überprüfung der Erschöpfung

Alternativ erhalten wir auch eine Erschöpfungsprüfung, wenn wir einen Rückgabetyp angeben

function toGerman4(value: NoYes): string {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return 'Nein';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return 'Ja';
  }
}

Wenn wir ein Mitglied zu NoYes hinzufügen, beschwert sich TypeScript, dass toGerman4() möglicherweise undefined zurückgibt.

Nachteile dieses Ansatzes

12.7.3 keyof und Enums

Wir können den keyof-Typoperator verwenden, um den Typ zu erstellen, dessen Elemente die Schlüssel der Enum-Mitglieder sind. Wenn wir dies tun, müssen wir keyof mit typeof kombinieren

enum HttpRequestKeyEnum {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}
// %inferred-type: "Accept" | "Accept-Charset" | "Accept-Datetime" |
// "Accept-Encoding" | "Accept-Language"
type HttpRequestKey = keyof typeof HttpRequestKeyEnum;

function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
  // ···
}
12.7.3.1 Verwendung von keyof ohne typeof

Wenn wir keyof ohne typeof verwenden, erhalten wir einen anderen, weniger nützlichen Typ

// %inferred-type: "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"
type Keys = keyof HttpRequestKeyEnum;

keyof HttpRequestKeyEnum ist dasselbe wie keyof number.

12.8 Danksagung