enum ist eines der ältesten und kontroversesten Features in TypeScript. Es ist eine der wenigen Spracherweiterungen, die echten JavaScript-Code emittiert — und genau das ist Segen und Fluch zugleich. Auf der einen Seite bekommst du benannte Konstanten mit eingebauter Typ-Sicherheit, geschlossenem Wertebereich und (bei numerischen Enums) Reverse-Mapping. Auf der anderen Seite verstoßen Enums gegen das Prinzip „TypeScript ist nur ein Typ-Layer über JavaScript", brechen mit modernen Build-Pipelines (isolatedModules, esbuild, Vite, SWC), sind nicht zuverlässig tree-shakable und haben Stolperfallen, die selbst erfahrene Entwickler regelmäßig kalt erwischen. Dieser Artikel zeigt alle Enum-Varianten — numerische, string, const, heterogene und ambient — erklärt das Runtime-Verhalten im Detail und führt die heute weit verbreitete Alternative mit as const-Objekt plus Union of Literals ein. Am Ende kennst du beide Welten und kannst pro Projekt fundiert entscheiden.

Was ein Enum überhaupt ist

Ein Enum (kurz für enumeration, Aufzählung) ist eine benannte Sammlung verwandter Konstanten. Statt im Code mit losen Zahlen oder Strings zu hantieren, gibst du jedem zulässigen Wert einen sprechenden Namen — und der Compiler stellt sicher, dass nur diese erlaubten Werte verwendet werden.

ts
// Ohne Enum — was bedeutet 0, 1, 2?
function setStatus(status: number) { /* ... */ }
setStatus(2); // ?

// Mit Enum — selbsterklärend, typsicher
enum Status {
  Idle,
  Loading,
  Success,
  Error,
}
function setStatus2(status: Status) { /* ... */ }
setStatus2(Status.Success); // klar lesbar

Anders als die meisten TypeScript-Features ist enum kein reines Typ-Konstrukt: Es erzeugt zur Laufzeit ein echtes JavaScript-Objekt mit den Werten. Genau diese Doppelrolle als Typ und Wert ist der Kern der Diskussion um Enums.

Numerische Enums

Die Default-Form. Ohne Initializer beginnen die Werte bei 0 und zählen automatisch hoch.

ts
enum Direction {
  North, // 0
  South, // 1
  East,  // 2
  West,  // 3
}

console.log(Direction.North); // 0
console.log(Direction.West);  // 3
Output
0
3

Du kannst auch explizit starten — alle folgenden Member zählen vom Startwert weiter:

ts
enum HttpStatus {
  Ok = 200,
  Created,        // 201
  Accepted,       // 202
  NoContent = 204 // Sprung erlaubt
}

Reverse-Mapping

Eine numerische Enum-Variable hat beide Richtungen: Name zu Wert und Wert zu Name. Der Compiler emittiert dafür ein Objekt mit Doppel-Keys.

ts
enum Direction {
  North, South, East, West,
}

console.log(Direction.North); // 0   — Name -> Wert
console.log(Direction[0]);    // "North" — Wert -> Name
console.log(Object.keys(Direction));
Output
0
North
[ '0', '1', '2', '3', 'North', 'South', 'East', 'West' ]

Das transpiliert zu folgendem JavaScript:

ts
var Direction;
(function (Direction) {
  Direction[Direction["North"] = 0] = "North";
  Direction[Direction["South"] = 1] = "South";
  Direction[Direction["East"]  = 2] = "East";
  Direction[Direction["West"]  = 3] = "West";
})(Direction || (Direction = {}));

Reverse-Mapping ist praktisch fürs Debugging, aber tückisch: Object.keys(Direction) liefert acht Einträge, nicht vier. Wer naiv über ein Enum iteriert, bekommt sowohl die Strings als auch die Zahlen.

String Enums

Jeder Member bekommt einen expliziten String-Wert. Kein Auto-Increment, kein Reverse-Mapping — dafür Werte, die im Log, in JSON-Serialisierung und in Browser-DevTools lesbar bleiben.

ts
enum Status {
  Idle    = "idle",
  Loading = "loading",
  Success = "success",
  Error   = "error",
}

const current: Status = Status.Loading;
console.log(current);                  // "loading"
console.log(JSON.stringify(current));  // "\"loading\""
console.log(Object.keys(Status));
Output
loading
"loading"
[ 'Idle', 'Loading', 'Success', 'Error' ]

String-Enums sind strikt: ein Status lässt sich nicht aus einem freien String konstruieren, auch wenn der Inhalt passt.

ts
const raw = "loading";
const s1: Status = raw;
//                 ^^^ Error: Type 'string' is not assignable to type 'Status'.

const s2: Status = Status.Loading;             // OK
const s3: Status = "loading" as Status;        // OK, aber Cast
const s4: Status = raw as Status;              // OK, aber gefährlich

Genau diese Striktheit ist beim Lesen externer Daten (API-Antworten, URL-Params) lästig: Du musst entweder casten oder explizit zwischen string und Status validieren.

Const Enums — Compile-Zeit-Inlining

Mit const enum verzichtest du auf das Runtime-Objekt. Der Compiler inlined alle Zugriffe direkt als Literal.

ts
const enum LogLevel {
  Debug, Info, Warn, Error,
}

const level = LogLevel.Warn;

Das emittierte JavaScript enthält kein LogLevel-Objekt:

ts
// Output:
const level = 2 /* LogLevel.Warn */;

Das spart Bundle-Bytes und Laufzeit-Zugriffe — klingt verlockend. In der Praxis bringt const enum aber mehr Probleme als Nutzen:

  • Es ist inkompatibel mit isolatedModules — und damit mit jedem modernen Single-File-Transpiler: esbuild, Vite, SWC, Babel können const enum nicht inlinen, weil sie pro Datei kompilieren und das Enum-Modul nicht querlesen.
  • In publizierten Libraries ist const enum brandgefährlich: Konsumenten, die mit anderen Versionen kompilieren, bekommen inkonsistente Werte.
  • Mit dem preserveConstEnums-Flag emittiert TypeScript das Runtime-Objekt doch — was den Sinn der Optimierung wieder aufhebt.

Faustregel: const enum nur in rein TypeScript-internen Projekten mit tsc als alleinigem Compiler. In jedem Vite/esbuild/SWC-Setup: meiden oder per ESLint-Regel (no-restricted-syntax) verbieten.

Heterogene Enums — technisch möglich, praktisch unsinnig

Du kannst Strings und Zahlen mischen. Das Handbook rät explizit davon ab.

ts
enum Mixed {
  No  = 0,
  Yes = "YES",
}

// Funktioniert, aber: was ist Mixed[0]? "No". Was ist Mixed["YES"]? undefined.
// Reverse-Mapping nur für numerische Member.

Heterogene Enums verlieren die Konsistenz beider Welten: Du bekommst weder das saubere String-Verhalten noch das Reverse-Mapping geschlossen. In TypeScript 5.0 wurde zudem die Validierung verschärft — ein numerischer Member, der indirekt auf einen String-Member zugreift, ist heute ein Compile-Error.

ts
// Seit TS 5.0:
enum Letters { A = "a" }
enum Numbers {
  one = 1,
  two = Letters.A, // Error: Computed values are not permitted
  //              in an enum with string valued members.
}

Computed Members und konstante Ausdrücke

Enum-Member-Werte können konstante Ausdrücke sein (Compile-Zeit berechenbar) oder computed values (zur Laufzeit). Die Unterscheidung ist relevant, weil sie das Verhalten als Typ beeinflusst.

Konstant sind:

  • Literal-Werte (Zahlen, Strings)
  • Verweise auf bereits definierte Member desselben Enums
  • Klammer-Ausdrücke daraus
  • Unary +, -, ~
  • Binary +, -, *, /, %, <<, >>, >>>, &, |, ^
ts
// Klassischer Bitflag-Einsatz von Enums:
enum Permission {
  None      = 0,
  Read      = 1 << 0, // 1
  Write     = 1 << 1, // 2
  Execute   = 1 << 2, // 4
  ReadWrite = Read | Write, // 3
  All       = Read | Write | Execute, // 7
}

const p: Permission = Permission.Read | Permission.Write;
console.log(p);                         // 3
console.log((p & Permission.Read) !== 0); // true
Output
3
true

Computed sind alle anderen Ausdrücke — Funktionsaufrufe, externe Variablen, Math.random(). Bis TypeScript 4.9 verloren Enums mit computed members ihre Union-Enum-Eigenschaft: Die Member ließen sich nicht mehr als eigene Typen verwenden. Seit TS 5.0 sind alle Enums Union-Enums.

Enum als Typ und als Wert

Eine enum-Deklaration erzeugt zwei Dinge gleichzeitig: einen Typ-Namen (im Typ-Raum) und einen Wert-Namen (im Wert-Raum). Beides heißt gleich.

ts
enum Color { Red, Green, Blue }

// Color als Typ:
function paint(c: Color): void { /* ... */ }

// Color als Wert (Objekt):
const all = Object.values(Color); // [0, 1, 2, "Red", "Green", "Blue"]

// keyof typeof — gibt die String-Schlüssel:
type ColorName = keyof typeof Color;   // "Red" | "Green" | "Blue"

// Color allein als Typ — gibt die Werte:
type ColorValue = Color;               // 0 | 1 | 2

Diese Doppelrolle ist mächtig und verwirrend zugleich. keyof typeof MyEnum gibt die Schlüssel (Strings), während der Enum-Name allein als Typ die Werte liefert. Wer das vertauscht, bekommt schwer lesbare Fehlermeldungen.

Die moderne Alternative: as const-Objekt + Union of Literals

Seit as const (TypeScript 3.4) gibt es ein idiomatisches JavaScript-Pattern, das die Funktion eines Enums ohne dessen Macken ersetzt.

ts
const Status = {
  Idle:    "idle",
  Loading: "loading",
  Success: "success",
  Error:   "error",
} as const;

// Typ aus dem Objekt ableiten:
type Status = typeof Status[keyof typeof Status];
// -> "idle" | "loading" | "success" | "error"

function setStatus(s: Status) {
  console.log(s);
}

setStatus(Status.Loading); // OK
setStatus("error");        // OK — String-Literal passt in Union
setStatus("foo");          // Error: not assignable to Status
Output
loading

Was du hier gewinnst:

  • Pures JavaScript — kein Spracherweiterung, kein Sondercompiler, keine Inkompatibilität mit esbuild/Vite/SWC.
  • Tree-shakable — ein nicht benutztes as const-Objekt fliegt beim Bundler raus.
  • Zuweisbar aus String-Literalen — keine Cast-Akrobatik bei API-Antworten.
  • Kein Reverse-Mapping-GeistObject.keys(Status) gibt genau vier Einträge.
  • Funktioniert in .d.ts ohne Sonderregeln.
  • Trivial migrierbar — die Call-Site Status.Loading bleibt gleich.

Was du verlierst:

  • Keine Auto-Inkrement-Bequemlichkeit (musst Werte explizit notieren).
  • Keinen abgeschlossenen „Namespace-Typ" wie Color ohne Werte-Bezug — du musst beides ableiten.
  • Etwas mehr Typ-Boilerplate (typeof X[keyof typeof X]).

Für viele Teams ist das ein klarer Netto-Gewinn. Das offizielle TypeScript-Handbook listet das Pattern in der Sektion „Objects vs Enums" explizit als modernen Ersatz auf.

Wann Enum, wann Union?

Kriteriumenumas const + Union
Build-Pipeline: esbuild / Vite / SWC− (const enum bricht)+
Build-Pipeline: nur tsc++
Bibliothek/Package zum Veröffentlichen− (Versionsfalle)+
Reverse-Mapping (Wert → Name) gewünscht+ (nur numerisch)− (manuell)
Bitflags / numerische Operationen+ (bitweise Ausdrücke)− (umstaendlicher)
API-Antworten als Strings zuweisen− (Cast nötig)+ (direkt)
Tree-Shaking− (meist nicht)+
Iteration über alle Werte− (Doppel-Keys)+ (Object.values)
Existiert in .d.ts problemlos! (mit Vorbehalt)+
Lesbarkeit für Neulinge+ (vertraut aus Java/C#)− (TS-Idiom)
Zukunfts-Sicherheit (JS-Spec)− (Spracherweiterung)+ (Plain JS)

Kurzregel: Für neue Projekte mit modernem Bundler ist as const + Union die Default-Wahl. enum bleibt sinnvoll in tsc-only-Backends, bei Bitflags und in bestehenden Codebasen, die das Pattern konsequent verwenden — Inkonsistenz ist meist teurer als die theoretischen Nachteile.

TypeScript 5.0 — Enum-Overhaul

TypeScript 5.0 hat Enums an mehreren Stellen verschärft und vereinheitlicht.

Alle Enums sind Union Enums. Bisher fielen Enums mit computed members auf den simplen number-Typ zurück — jetzt behalten alle Member ihren eigenen Literal-Typ, auch wenn der Wert zur Laufzeit berechnet wird.

ts
enum E {
  Blah = Math.random(),
}
// Seit TS 5.0: E.Blah ist ein eigener Typ, kein 'number'-Fallback
let x: E.Blah = E.Blah; // OK

Striktere Zuweisungs-Prüfung. Werte, die zufällig „passen" würden, aber kein Member sind, sind jetzt ein Error.

ts
enum EvenDigit {
  Zero = 0, Two = 2, Four = 4,
}

// Vor TS 5.0: still erlaubt
// Seit TS 5.0:
const m: EvenDigit = 1;
//    ^ Error: Type '1' is not assignable to type 'EvenDigit'.

Heterogene Member korrekt erkannt. Indirekte String-zu-Number-Übergaben innerhalb von Enums werden jetzt als Fehler erkannt — ein Loch im alten Type-Checker.

Diese Änderungen machen Enums in TS 5+ deutlich weniger fehleranfällig. Trotzdem bleiben die strukturellen Nachteile (Bundler-Inkompatibilität, Tree-Shaking, .d.ts-Probleme) bestehen — sie sind nicht im Type-Checker zu lösen, sondern in der Emit-Strategie verankert.

Ambient Enums in .d.ts-Dateien

Wenn du externe JavaScript-Bibliotheken typisierst, kannst du Enums mit declare enum ankündigen, ohne sie zu implementieren.

ts
// types/lib.d.ts
declare enum LegacyStatus {
  Active   = 1,
  Inactive = 2,
}

Achtung: Uninitialisierte Member in ambient Enums werden vom Compiler als computed behandelt — anders als bei normalen Enums. Das bedeutet: Wenn du in einem .d.ts declare enum X &#123; A, B &#125; schreibst, kannst du X.A und X.B nicht zuverlässig als Literal-Typen verwenden. Besser: alle Werte explizit angeben.

declare const enum ist eine weitere Falle — Konsumenten ohne isolatedModules: false oder ohne preserveConstEnums bekommen Fehler. Für publizierte Type-Definitionen gilt: kein const enum exportieren.

Häufige Stolperfallen

const enum bricht mit isolatedModules / Vite / esbuild / SWC.

Single-File-Transpiler kompilieren jede Datei isoliert und können Werte aus anderen Modulen nicht inlinen. Der TS-Flag isolatedModules: true macht das explizit zum Error. Konsequenz: In jedem modernen Bundler-Setup ist const enum faktisch unbenutzbar. Workaround ist preserveConstEnums — was den Sinn der Optimierung (kein Runtime-Objekt) aushebelt. Praxis-Regel: const enum per ESLint-Regel verbieten.

Reverse-Mapping bei numerischen Enums versteckt Vergleichs-Bugs.

Object.keys(NumericEnum) liefert sowohl die Namen als auch die Werte als Strings — wer naiv über das Objekt iteriert, bekommt jeden Eintrag doppelt. Zudem ist "0" in MyEnum wahr, obwohl "0" kein Enum-Name ist. Wer Enum-Iteration braucht, sollte nach typeof key === "string" && isNaN(Number(key)) filtern — oder gleich ein as const-Objekt verwenden.

Numerische Enums haben Doppel-Keys im Runtime-Objekt.

Das emittierte Objekt ist { 0: "A", A: 0, ... }. Object.values(MyEnum).length ergibt das Doppelte der Member-Anzahl. String-Enums haben das Problem nicht — sie haben kein Reverse-Mapping. Ein weiterer Grund, für Status-/Variant-Werte konsequent String-Enums oder as const-Objekte zu nutzen.

String-Enum-Vergleich akzeptiert keine freien Strings.

Ein API-Endpoint liefert "loading" als String, dein Code erwartet Status. Direkte Zuweisung ist ein Fehler — du musst casten (as Status) oder validieren. Mit as const + Union of Literals entfaellt das: String-Literale sind direkt zuweisbar. Genau dieser Reibungsverlust ist einer der Hauptgruende, warum API-nahe Codebasen heute auf as const umsteigen.

Enum-Werte ändern ist ein Runtime-Breaking-Change.

Weil Enums zur Laufzeit existieren, ist das Umbenennen eines Members oder das Verschieben einer Zahl kein reiner Typ-Refactor — es ändert serialisierte Daten, persistierte Werte, API-Verträge. Bei numerischen Enums ohne explizite Werte verschieben sich alle Folgewerte, wenn du einen Member einfügst. Best Practice: numerische Enums immer mit explizit gesetzten Werten, String-Enums mit stabilen Strings.

keyof typeof MyEnum vs. MyEnum — Schlüssel vs. Werte.

keyof typeof Status ergibt die Strings "Idle" | "Loading" | ... (Member-Namen). Status als Typ allein ergibt die Werte ("idle" | "loading" | ... oder die Zahlen). Wer eine Funktion mit String-Schlüsseln parametrisiert, will fast immer keyof typeof. Wer den eigentlichen Wert uebergibt, will den Enum-Typ direkt. Verwechselungen produzieren Fehler wie Type '"Idle"' is not assignable to type 'Status'.

Enums sind nicht zuverlaessig tree-shakable.

Auch wenn nur ein Member benutzt wird, behaelt der Bundler oft das gesamte Enum-Objekt — weil die IIFE-Form ((function (E) { ... })(E || (E = {}))) Seiteneffekte hat, die statische Analyse nicht durchschauen kann. as const-Objekte sind reine Property-Assignments und werden vom Bundler problemlos eliminiert, wenn nur einzelne Eintraege verwendet werden.

enum in .d.ts ist problematisch — const enum erst recht.

Konsumenten deiner Type-Definitionen erwarten in der Regel reine Typen, kein Runtime-Verhalten. Ein enum X in einer Library-.d.ts verlangt, dass das implementierende Modul ein passendes Objekt exportiert — sonst undefined zur Laufzeit. declare const enum bricht bei jedem Konsumenten mit isolatedModules: true. Empfehlung für Library-Autoren: keine Enums exportieren, stattdessen Union-Typen aus String-Literalen.

Migration enum -> as const ist meist trivial.

Die Call-Site Status.Loading bleibt identisch — nur die Deklaration wird zu const Status = { Loading: "loading", ... } as const plus type Status = typeof Status[keyof typeof Status]. Funktions-Signaturen, Vergleiche, Switch-Cases funktionieren unverändert. Einzige Stolperstelle: Reverse-Mapping (NumericEnum[0]) muss manuell über ein zweites Objekt nachgebildet werden — oft aber ohnehin nicht gebraucht.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Komplexe Typen

Zur Übersicht