Enums vs. Union-Literal-Typen in TypeScript: der Vergleich
AI generated
type
TypeScript
Enums vs. Union-Literal-Typen: was wann verwenden
Zwei Wege zu einer festen Menge erlaubter Werte, mit unterschiedlichen Kompromissen

TypeScript bietet mit Enums und Union-Literal-Typen zwei grundverschiedene Werkzeuge, um eine feste Menge erlaubter Werte abzubilden. Beide haben ihre Berechtigung, doch die Wahl hat spürbare Auswirkungen auf Bundle-Größe, Serialisierung und Interoperabilität.

9 Min. Lesezeit Sprachdesign Best Practices

1. Zwei Ansätze für eine feste Wertemenge

Sowohl Enums als auch Union-Literal-Typen lösen dasselbe Grundproblem: eine Variable soll nur einen von mehreren vordefinierten Werten annehmen dürfen. Wie dieses Ziel erreicht wird, unterscheidet sich jedoch fundamental. Enums erzeugen eine echte Laufzeit-Konstruktion, Union-Literal-Typen existieren dagegen ausschließlich im Typsystem.

Diese unterschiedliche Natur zieht Konsequenzen nach sich, die weit über reine Geschmacksfragen hinausgehen: Bundle-Größe, Verhalten bei der JSON-Serialisierung, Debugging-Erfahrung und Interoperabilität mit reinem JavaScript-Code sind alle betroffen.

Gerade in Teams, die von anderen Sprachen mit klassischen Enums zu TypeScript wechseln, ist die Versuchung groß, reflexartig zu Enums zu greifen. Ein bewusster Vergleich beider Ansätze zahlt sich jedoch aus, bevor sich eine Konvention im gesamten Projekt verfestigt.

2. Numerische Enums und ihre Eigenheiten

Ein numerisches Enum erzeugt zur Laufzeit ein Objekt mit einer sogenannten Reverse-Mapping-Eigenschaft: Nicht nur der Name zeigt auf den Wert, auch der Wert zeigt zurück auf den Namen. Das ist praktisch für Debugging-Ausgaben, führt aber zu doppelt so vielen Einträgen im erzeugten JavaScript-Objekt.

Ein weiteres Risiko numerischer Enums ist ihre implizite Kompatibilität mit jeder beliebigen Zahl. TypeScript erlaubt es, eine beliebige number-Variable an eine Stelle zu übergeben, die ein numerisches Enum erwartet, ohne dass der Compiler dies beanstandet.

Hinzu kommt, dass sich die automatisch vergebenen numerischen Werte bei nachträglichem Einfügen eines neuen Members verschieben können, was bereits gespeicherte Werte in einer Datenbank oder in einer JSON-Datei stillschweigend falsch interpretierbar macht, wenn nicht jeder Member explizit einen festen Wert erhält.


enum Status {
  Draft,
  Published,
  Archived,
}

console.log(Status.Published); // 1
console.log(Status[1]);        // "Published" (Reverse Mapping)

function setStatus(s: Status) {}
setStatus(42); // wird vom Compiler nicht beanstandet

3. String-Enums und const enum

String-Enums vermeiden das Reverse-Mapping-Problem und die implizite Kompatibilität mit beliebigen Zahlen, da jeder Member einen expliziten String-Wert erhält. Sie sind damit deutlich sicherer als numerische Enums, erzeugen aber weiterhin ein echtes Laufzeit-Objekt.

Ein const enum wird dagegen zur Kompilierzeit komplett aufgelöst und hinterlässt keinerlei Laufzeit-Objekt: Jede Verwendung wird direkt durch den Literal-Wert ersetzt. Das spart Bundle-Größe, funktioniert aber nicht mit der isolatedModules Compiler-Option, die von Tools wie esbuild, swc oder Babel benötigt wird, da diese Dateien isoliert transpilieren, ohne den Enum-Wert kennen zu können.


enum ExportFormat {
  Json = "json",
  Xml = "xml",
  Csv = "csv",
}

const enum LogLevel {
  Info = "info",
  Warn = "warn",
  Error = "error",
}

// const enum wird bei der Kompilierung durch "info" ersetzt
console.log(LogLevel.Info);

4. Union-Literal-Typen als Alternative

Ein Union-Literal-Typ definiert die erlaubte Wertemenge rein im Typsystem, ohne jegliche Laufzeit-Repräsentation. Zur Laufzeit sind die Werte einfache Strings oder Zahlen, was die Interoperabilität mit reinem JavaScript-Code, JSON-APIs und Bibliotheken ohne eigene Enum-Unterstützung erheblich vereinfacht.

Da kein zusätzliches Objekt erzeugt wird, entfällt auch jeglicher Bundle-Size-Overhead. Für Discriminated Unions, ein zentrales Muster in typsicherem TypeScript-Code, sind Union-Literal-Typen ohnehin die natürliche Wahl.


type ExportFormat = "json" | "xml" | "csv";

function exportData(format: ExportFormat) {
  switch (format) {
    case "json":
      return toJson();
    case "xml":
      return toXml();
    case "csv":
      return toCsv();
  }
}

5. Strukturelle vs. nominale Eigenschaften

Ein oft übersehener Unterschied betrifft die Typprüfung selbst. Enums verhalten sich in TypeScript teilweise nominal: Zwei unterschiedliche Enums mit identischen Werten sind nicht gegenseitig zuweisbar, selbst wenn die zugrunde liegenden Werte übereinstimmen. Union-Literal-Typen dagegen sind rein strukturell, ein String-Literal ist immer kompatibel, egal aus welchem Kontext es stammt.

Dieses nominale Verhalten kann in großen Codebasen tatsächlich als Feature dienen, um versehentliche Verwechslungen ähnlicher Wertemengen zu verhindern, erschwert aber gleichzeitig die Interoperabilität mit externen Daten.

Wer beide Vorteile verbinden möchte, greift häufig auf Branded Types zurück, eine Technik, die auf Union-Literal-Typen aufbaut, aber zusätzlich ein künstliches Unterscheidungsmerkmal einführt, um versehentliche Verwechslungen ähnlicher String-Typen zu verhindern, ohne dabei die Nachteile echter Enums in Kauf zu nehmen.

6. JSON-Serialisierung und API-Grenzen

An API-Grenzen, etwa beim Empfang von JSON-Daten aus einem Backend, kommen niemals echte Enum-Instanzen an, sondern immer einfache Strings oder Zahlen. Bei Union-Literal-Typen entspricht das exakt der Laufzeit-Repräsentation, eine Konvertierung ist unnötig.

Bei Enums muss der empfangene Rohwert dagegen explizit gegen die erlaubten Enum-Werte geprüft oder in den Enum-Typ umgewandelt werden, was zusätzlichen Code an jeder Systemgrenze erfordert.

Diese zusätzliche Konvertierungslogik summiert sich in größeren Anwendungen mit vielen API-Endpunkten schnell zu spürbarem Mehraufwand, der bei konsequenter Verwendung von Union-Literal-Typen von Anfang an entfällt.


type Status = "draft" | "published" | "archived";

async function fetchStatus(): Promise<Status> {
  const res = await fetch("/api/status");
  const data = await res.json();
  return data.status as Status; // direkt kompatibel
}

7. Wann Enums tatsächlich sinnvoll sind

Trotz der genannten Nachteile haben Enums weiterhin legitime Anwendungsfälle. Bei Bitflag-Mustern, bei denen mehrere Werte über bitweise Operationen kombiniert werden, sind numerische Enums nach wie vor praktisch, da Union-Literal-Typen keine native Unterstützung für solche Kombinationen bieten.

Auch wenn eine echte Laufzeit-Iteration über alle möglichen Werte benötigt wird, etwa um ein Dropdown-Menü dynamisch zu befüllen, bietet ein Enum-Objekt diese Möglichkeit direkt, während bei einem Union-Literal-Typ eine separate Konstante mit allen Werten gepflegt werden muss.

Auch in generiertem Code, etwa aus Protokoll-Definitionen wie Protocol Buffers oder GraphQL-Schemas, tauchen häufig Enums als natives Ausgabeformat auf, sodass sich eine Umstellung auf Union-Literal-Typen in solchen Fällen nicht lohnt, wenn der generierte Code ohnehin nicht manuell gepflegt wird.

8. Praktische Empfehlung

Für die meisten Anwendungsfälle in modernem TypeScript-Code, insbesondere bei Discriminated Unions, API-Antworttypen und Konfigurationswerten, sind Union-Literal-Typen die pragmatischere Wahl: kein Laufzeit-Overhead, einfache Serialisierung, volle Kompatibilität mit reinem JavaScript.

Ist eine echte Laufzeit-Iteration über alle Werte nötig, bietet sich ein Muster mit einem as const Array kombiniert mit einem daraus abgeleiteten Union-Typ an, das die Vorteile beider Welten vereint, ohne ein echtes Enum zu benötigen.


const STATUSES = ["draft", "published", "archived"] as const;
type Status = (typeof STATUSES)[number];

// STATUSES.map(...) ermöglicht Laufzeit-Iteration
// Status bleibt ein reiner Union-Literal-Typ

9. Fallstricke bei der Migration

Beim Ersetzen bestehender Enums durch Union-Literal-Typen sollte man beachten, dass const enum Werte in Downstream-Code oft direkt als Zahlen oder Strings verglichen werden. Eine Migration erfordert daher eine sorgfältige Suche nach allen Verwendungsstellen, insbesondere bei numerischen Enums, deren konkrete Werte oft implizit vorausgesetzt werden.

Ein weiterer Stolperstein: const enum funktioniert nicht in Projekten mit isolatedModules, was seit TypeScript 5.0 sogar für viele Standard-Konfigurationen der Default ist. Wer const enum weiterhin nutzen möchte, muss diese Option explizit deaktivieren, was wiederum andere Build-Tools einschränken kann.

Auch bei Tests lohnt sich ein zweiter Blick: Snapshot-Tests, die auf dem Reverse-Mapping numerischer Enums basieren, brechen häufig unbemerkt, wenn die Reihenfolge der Enum-Member verändert wird. Union-Literal-Typen sind von diesem Risiko naturgemäß nicht betroffen, da sie keine automatisch generierten numerischen Werte besitzen.

Für eine sichere Migration empfiehlt sich zusätzlich eine automatisierte Suche nach allen Importstellen des betroffenen Enums, kombiniert mit einer Übergangsphase, in der beide Typdefinitionen parallel existieren, bis die Umstellung vollständig abgeschlossen ist.

Merkmal Numerisches Enum String-Enum const enum Union-Literal-Typ
Laufzeit-Objekt Ja, mit Reverse Mapping Ja Nein, wird inline ersetzt Nein
Bundle-Overhead Ja Ja Keiner Keiner
Kompatibel mit isolatedModules Ja Ja Nein Ja
JSON-Serialisierung Erfordert Konvertierung Erfordert Konvertierung Direkt kompatibel Direkt kompatibel
Typprüfung Teilweise nominal Teilweise nominal Teilweise nominal Rein strukturell

Mironsoft

TypeScript-Migration, Typsicherheit und Team-Onboarding

JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?

Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.

Migrations-Fahrplan

Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.

Strict-Mode-Einführung

tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.

Team-Onboarding

Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.

10. Zusammenfassung

Enums vs. Union-Literale

Laufzeit-Overhead Enum

Echtes Objekt pro Enum

Laufzeit-Overhead Union

Keiner, reines Typsystem

const enum Einschränkung

Inkompatibel mit isolatedModules

Empfehlung

Union-Literale als Standardwahl

11. FAQ: Enums vs. Union-Literale

1Was ist der grundlegende Unterschied zwischen Enums und Union-Literal-Typen?
Enums erzeugen ein echtes Laufzeit-Objekt in JavaScript, während Union-Literal-Typen ausschließlich im Typsystem existieren und zur Laufzeit einfache Werte wie Strings oder Zahlen bleiben. Das wirkt sich direkt auf Bundle-Größe und Interoperabilität aus.
2Warum gelten numerische Enums als riskanter als String-Enums?
Numerische Enums erlauben es dem Compiler, beliebige Zahlen als gültige Enum-Werte zu akzeptieren, was Tippfehler oder falsche Werte unentdeckt lässt. String-Enums sind davon nicht betroffen und gelten deshalb als sicherer.
3Was macht const enum anders als ein normales Enum?
const enum wird vollständig zur Kompilierzeit aufgelöst und erzeugt kein Laufzeit-Objekt. Jede Verwendung wird direkt durch den Literal-Wert ersetzt, was Bundle-Größe spart, aber Einschränkungen bei bestimmten Build-Tools mit sich bringt.
4Warum funktioniert const enum nicht mit isolatedModules?
Tools, die mit isolatedModules arbeiten, transpilieren jede Datei isoliert und kennen daher den konkreten Wert eines const enum aus einer anderen Datei nicht, wodurch die Inline-Ersetzung nicht möglich ist und ein Fehler gemeldet wird.
5Warum sind Union-Literal-Typen für JSON-APIs oft die bessere Wahl?
Da Union-Literal-Typen zur Laufzeit einfache Strings oder Zahlen sind, entspricht dies exakt dem Format von JSON-Daten, sodass keine Konvertierung zwischen Rohdaten und Enum-Instanzen an jeder Systemgrenze nötig ist.
6Was bedeutet nominales Verhalten bei Enums?
Zwei unterschiedliche Enums mit identischen Werten sind in TypeScript nicht gegenseitig zuweisbar, obwohl die zugrunde liegenden Werte übereinstimmen. Union-Literal-Typen verhalten sich dagegen rein strukturell und sind dadurch flexibler kompatibel.
7Wann sind Enums trotzdem sinnvoll?
Bei Bitflag-Mustern mit bitweisen Kombinationen mehrerer Werte sowie in Fällen, in denen eine echte Laufzeit-Iteration über alle Werte benötigt wird, bieten Enums direkte Unterstützung, die Union-Literal-Typen von Haus aus nicht mitbringen.
8Wie erreicht man Laufzeit-Iteration mit Union-Literal-Typen?
Über ein as const Array, aus dem sowohl der Union-Typ mittels typeof und Indexzugriff abgeleitet als auch eine echte Laufzeit-Liste für Iterationen genutzt werden kann, ohne ein echtes Enum zu benötigen.
9Welche Migrationsrisiken bestehen beim Wechsel von Enums zu Union-Literalen?
Bestehender Code, der konkrete Enum-Werte implizit voraussetzt, insbesondere bei numerischen Enums, muss sorgfältig geprüft werden, da sich das Laufzeitverhalten beim Wechsel auf Union-Literal-Typen an jeder Verwendungsstelle ändert.
10Was ist die generelle Empfehlung für neue TypeScript-Projekte?
Für die meisten Anwendungsfälle, insbesondere Discriminated Unions und API-Typen, sind Union-Literal-Typen die pragmatischere Wahl. Enums bleiben für Bitflags und explizite Laufzeit-Iteration relevant und sollten gezielt eingesetzt werden.