Enums in TypeScript: Feste Wertemengen typsicher abbilden

Enums bündeln zusammengehörige Konstanten unter einem Namen. Du lernst numerische und String-Enums, ihre Fallstricke zur Laufzeit und wann ein union type die bessere Wahl ist.

Teilen

Sobald ein Wert nur einen von wenigen festen Zuständen annehmen darf – ein Bestellstatus, eine Himmelsrichtung, eine Nutzerrolle – greifen viele Einsteiger zu losen Strings wie "aktiv" oder "inaktiv". Das funktioniert, bis sich ein Tippfehler einschleicht und der Compiler nichts bemerkt. Genau hier kommen Enums ins Spiel: Sie geben einer Menge zusammengehöriger Konstanten einen gemeinsamen Namen und lassen TypeScript prüfen, ob du wirklich einen erlaubten Wert verwendest. In diesem Beitrag schauen wir uns numerische und String-Enums an, verstehen, was Enums zur Laufzeit tatsächlich sind, und klären, wann du besser auf einen union type ausweichst.

Numerische Enums: der Standardfall

Ein Enum definierst du mit dem Schlüsselwort enum. Wenn du keine Werte zuweist, nummeriert TypeScript die Mitglieder automatisch ab 0 durch. Das ist praktisch, wenn dir der konkrete Zahlenwert egal ist und du nur unterscheidbare Zustände brauchst.

enum Richtung {
  Norden, // 0
  Osten,  // 1
  Sueden, // 2
  Westen, // 3
}

function bewege(r: Richtung): void {
  console.log(`Gehe nach Richtung mit Code ${r}`);
}

bewege(Richtung.Norden); // Gehe nach Richtung mit Code 0

Du kannst den Startwert auch selbst festlegen. Setzt du das erste Mitglied auf einen Wert, zählt TypeScript ab dort weiter. Das ist nützlich, wenn deine Codes eine fachliche Bedeutung haben, etwa HTTP-ähnliche Statuscodes:

enum Status {
  Aktiv = 1,
  Pausiert,   // 2
  Beendet,    // 3
}

console.log(Status.Pausiert); // 2

String-Enums: lesbar und stabil

Numerische Enums haben einen Haken: In Logs oder in einer Datenbank steht am Ende nur eine nackte 2, deren Bedeutung niemand kennt. String-Enums lösen das, indem jedes Mitglied einen sprechenden String bekommt. Hier musst du jeden Wert explizit angeben – dafür bleiben die Werte auch stabil, wenn du später Mitglieder umsortierst.

enum Rolle {
  Admin = "ADMIN",
  Redakteur = "REDAKTEUR",
  Leser = "LESER",
}

function darfBearbeiten(rolle: Rolle): boolean {
  return rolle === Rolle.Admin || rolle === Rolle.Redakteur;
}

console.log(darfBearbeiten(Rolle.Leser)); // false

Für die meisten Anwendungsfälle sind String-Enums die bessere Wahl: Der Wert erklärt sich beim Debuggen selbst, und du bist gegen Verschiebungen in der Reihenfolge abgesichert.

Was ein Enum zur Laufzeit ist

Ein wichtiger Unterschied zu vielen anderen TypeScript-Konstrukten: Ein normales Enum verschwindet nicht beim Kompilieren, sondern wird zu echtem JavaScript-Code. TypeScript erzeugt ein Objekt, über das du zur Laufzeit auf die Mitglieder zugreifen kannst.

enum Ampel {
  Rot = "ROT",
  Gelb = "GELB",
  Gruen = "GRUEN",
}

// Über alle Werte iterieren
for (const wert of Object.values(Ampel)) {
  console.log(wert); // ROT, GELB, GRUEN
}

Bei numerischen Enums fügt TypeScript zusätzlich ein reverse mapping hinzu: Du kommst nicht nur vom Namen zum Wert, sondern auch vom Wert zurück zum Namen. Das erklärt, warum Object.values dort auf den ersten Blick doppelt so viele Einträge liefert.

enum Level {
  Niedrig,  // 0
  Hoch,     // 1
}

console.log(Level.Hoch);    // 1
console.log(Level[1]);      // "Hoch"  (reverse mapping)

const enum: schlanker Output

Wenn du das erzeugte Laufzeit-Objekt gar nicht brauchst und nur die Werte an Ort und Stelle einsetzen willst, gibt es const enum. Der Compiler ersetzt jede Verwendung direkt durch den konkreten Wert (Inlining) und generiert kein zusätzliches Objekt. Das spart Bytes im fertigen Bundle.

const enum Groesse {
  Klein = "S",
  Mittel = "M",
  Gross = "L",
}

const t = Groesse.Mittel;
// wird im JS-Output einfach zu: const t = "M";

Der Preis dafür: Weil kein Objekt existiert, kannst du nicht zur Laufzeit über die Mitglieder iterieren. In manchen Build-Setups verursachen const enums außerdem Probleme, deshalb solltest du sie nur gezielt einsetzen.

Enum oder union type?

Nicht immer ist ein Enum die beste Lösung. Für viele Fälle reicht ein union type aus String-Literalen. Er existiert rein auf Typ-Ebene, erzeugt also keinerlei Laufzeit-Code, und die Werte sind direkt lesbar.

type Zahlungsart = "karte" | "paypal" | "rechnung";

function bezahle(art: Zahlungsart): void {
  console.log(`Bezahle per ${art}`);
}

bezahle("paypal");   // ok
bezahle("bitcoin");  // Fehler: nicht Teil des Typs

Als grobe Orientierung: Nimm einen union type, wenn du nur eine feste Auswahl an Werten typsicher machen willst und kein Objekt zur Laufzeit brauchst. Greif zum Enum, wenn du die Konstanten unter einem Namensraum bündeln, über sie iterieren oder das reverse mapping nutzen möchtest. Die wichtigsten Unterschiede auf einen Blick:

  • Union type: kein Laufzeit-Code, sehr leichtgewichtig, ideal für einfache Auswahlmengen.
  • String-Enum: gebündelter Namensraum, iterierbar, gut lesbare Werte beim Debuggen.
  • Numerisches Enum: kompakte Codes plus reverse mapping, aber wenig aussagekräftige Werte.
  • const enum: maximal schlanker Output durch Inlining, dafür keine Iteration möglich.

Fazit

Enums sind ein solides Werkzeug, um feste Wertemengen typsicher abzubilden und zusammengehörige Konstanten unter einem Namen zu bündeln. In der Praxis fährst du mit String-Enums am zuverlässigsten, weil ihre Werte lesbar und stabil bleiben. Behalte im Kopf, dass normale Enums echten Laufzeit-Code erzeugen – das ist mal Vorteil (Iteration, reverse mapping), mal unnötiger Ballast. Für schlichte Auswahlmengen ist oft ein union type die elegantere, leichtere Lösung. Wenn du beide Optionen kennst, triffst du für jeden Fall die passende Wahl.