In TypeScript ist nicht nur string ein Typ — auch der konkrete Wert "north" kann ein Typ sein. Literal Types heben einzelne Werte aus den Primitiv-Typen heraus und machen sie zu eigenständigen, nominal anmutenden Typen, die der Compiler an jeder Stelle einzeln prüfen kann. In Kombination mit Union-Typen ersetzen sie klassische Enums, modellieren API-Status, Routen-Schlüssel und Theme-Token typsicher — und sind ohne jeden Laufzeit-Overhead, weil sie reine Compile-Time-Konstrukte sind. Damit der Compiler einen Literal-Typ auch wirklich behält, muss man Widening verstehen: den Effekt, dass let-Variablen und Objekt-Properties standardmäßig auf den Basistyp aufgeweitet werden. Das Werkzeug der Wahl, um Literal-Typen über Strukturen hinweg zu erhalten, heißt as const — ergänzt durch das satisfies-Schlüsselwort seit TypeScript 4.9 für mehr Flexibilität.
Was Literal Types sind
In klassischen Typsystemen ist ein Typ eine Menge möglicher Werte: string umfasst alle Strings, number alle Zahlen, boolean genau zwei Werte. Literal Types drehen die Sichtweise um — sie definieren einen Typ als Menge mit genau einem Element.
type Yes = "yes"; // genau ein zulässiger Wert: "yes"
type Pi = 3.14159; // genau eine Zahl
type On = true; // genau ein Boolean
const a: Yes = "yes"; // OK
const b: Yes = "no"; // Error: Type '"no"' is not assignable to type '"yes"'.Für sich allein wirkt ein einzelner Literal-Typ wie reine Dokumentation — was bringt ein Typ, der nur einen Wert zulässt? Die Antwort liegt in der Kombination: erst als Bestandteil von Union-Typen, Discriminated Unions und generischen Constraints entfalten Literal Types ihre Wirkung. Sie ersetzen enum ohne Runtime-Code, machen Funktions-Signaturen exakter und sind das Fundament für Template Literal Types und konditionale Typ-Logik.
String Literal Types
Der häufigste Anwendungsfall: ein Funktions-Parameter darf nur ganz bestimmte Strings annehmen. Statt string notierst du die erlaubten Werte als Union.
type Direction = "north" | "south" | "east" | "west";
function move(direction: Direction): void {
console.log(`Bewege nach ${direction}`);
}
move("north"); // OK
move("up"); // Error: Argument of type '"up"' is not assignable
// to parameter of type 'Direction'.Der Vorteil gegenüber string ist doppelt: Der Compiler verhindert Tippfehler, und Auto-Completion in jeder IDE zeigt die zulässigen Werte direkt an. Auch das Refactoring profitiert — wird "west" umbenannt, findet die IDE alle Aufrufstellen, weil sie nicht nach einem String, sondern nach einem Typ-Vorkommen sucht.
Number Literal Types
Zahlen-Literale funktionieren analog. Klassisches Beispiel ist die Comparator-Funktion mit Rückgabetyp -1 | 0 | 1, ein anderes der Würfelwurf.
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
function roll(): Dice {
return (Math.floor(Math.random() * 6) + 1) as Dice;
}
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
type HttpStatus = 200 | 201 | 204 | 400 | 401 | 404 | 500;Number Literal Types sind besonders nützlich für endliche Wertebereiche mit fester Semantik: HTTP-Status, Z-Index-Stufen eines Design-Systems, Versionsnummern eines Schema-Migrators. Wo „eine beliebige Zahl" zu lax ist und ein Enum zu schwergewichtig wäre, liefern Number Literals die richtige Granularität.
Boolean Literal Types
Boolean Literals sind die kleinste denkbare Variante: nur true oder nur false. Der Typ boolean ist intern exakt die Union true | false.
type Yes = true;
type No = false;
// Sinnvoll als Discriminant in Union-Typen:
type Success = { ok: true; value: string };
type Failure = { ok: false; error: string };
type Result = Success | Failure;
function handle(r: Result) {
if (r.ok) {
console.log(r.value); // narrowed auf Success
} else {
console.log(r.error); // narrowed auf Failure
}
}Ein einzelnes true oder false als Typ macht außerhalb von Discriminated Unions selten Sinn — dort aber ist es das idiomatische Werkzeug, um zwei Varianten anhand eines Boolean-Flags auseinanderzuhalten.
Widening und Narrowing
Hier wird es subtil. TypeScript unterscheidet zwischen const- und let-Deklarationen — und zwar nicht nur syntaktisch, sondern auch beim inferierten Typ.
const x = "hello"; // Typ: "hello" (Literal Type)
let y = "hello"; // Typ: string (gewidet)
const dice = 6; // Typ: 6
let step = 6; // Typ: numberLogik dahinter: Eine const-Variable kann nicht neu zugewiesen werden, also bleibt ihr Wert für immer "hello" — der Compiler darf den präzisen Literal-Typ vergeben. Eine let-Variable kann jeden anderen String aufnehmen, also wird der Typ auf den Basistyp string geweitet (Widening).
Widening passiert auch innerhalb von Objekt-Literalen, selbst wenn das umgebende Konstrukt const ist:
const req = { url: "/api", method: "GET" };
// Typ: { url: string; method: string }
// Nicht: { url: "/api"; method: "GET" }
function fetchRequest(method: "GET" | "POST"): void {}
fetchRequest(req.method);
// ^^^^^^^^^^ Error: Argument of type 'string'
// is not assignable to parameter of type '"GET" | "POST"'.Der Grund: Properties eines const-Objekts sind sehr wohl mutierbar (req.method = "POST" ist erlaubt), also darf der Compiler den Literal-Typ nicht festschreiben.
Narrowing ist der gegenläufige Mechanismus: Innerhalb eines Type Guards engt TypeScript den Typ wieder auf einen Literal-Typ ein.
function handle(s: string) {
if (s === "yes") {
// s: "yes" — eingeengt auf den Literal
} else {
// s: string — alle anderen Strings
}
}as const — Widening verhindern
Das idiomatische Werkzeug, um Literal-Typen über Strukturen hinweg zu erhalten, ist die Const Assertion as const. Sie macht drei Dinge gleichzeitig:
- Primitiv-Werte behalten ihren Literal-Typ statt zum Basistyp zu widen.
- Objekt-Properties werden
readonlyund behalten ihre Literal-Typen. - Arrays werden zu
readonly-Tupeln mit Literal-Element-Typen.
const req1 = { url: "/api", method: "GET" };
// Typ: { url: string; method: string }
const req2 = { url: "/api", method: "GET" } as const;
// Typ: { readonly url: "/api"; readonly method: "GET" }
const arr1 = [1, 2, 3];
// Typ: number[]
const arr2 = [1, 2, 3] as const;
// Typ: readonly [1, 2, 3]Mit as const löst sich das Widening-Problem aus dem vorigen Abschnitt:
const req = { url: "/api", method: "GET" } as const;
function fetchRequest(method: "GET" | "POST"): void {}
fetchRequest(req.method); // OK — req.method ist "GET"as const wirkt tief: Auch verschachtelte Objekte und Arrays werden vollständig zu Literal-Typen mit readonly-Modifier. Das ist mächtig, aber auch streng — wer ein as const-Objekt an eine Funktion übergibt, die ein nicht-readonly-Objekt erwartet, bekommt einen Fehler. In der Praxis ist genau das oft erwünscht, weil es lokale Immutabilität durchsetzt.
Template Literal Types
Seit TypeScript 4.1 lassen sich Literal-Typen komponieren. Ein Template Literal Type setzt aus statischen Strings und Typ-Platzhaltern einen neuen Literal-Typ zusammen.
type Greeting = `hello ${string}`;
const g1: Greeting = "hello world"; // OK
const g2: Greeting = "hi there"; // Error
type Lang = "de" | "en" | "fr";
type Locale = `${Lang}-${Uppercase<Lang>}`;
// "de-DE" | "de-EN" | ... | "fr-FR"Template Literal Types sind ein eigenes Kapitel — sie verdienen einen separaten Artikel. Wichtig hier: Sie sind kompositorische Verkettungen von Literal-Typen und erweitern das, was du in diesem Artikel über Literal Types lernst, zu einem vollständigen String-Typsystem.
Literal Types in Discriminated Unions
Der mächtigste Einsatzbereich von Literal-Typen sind Discriminated Unions: Mehrere Objekt-Varianten teilen sich eine gemeinsame Property mit Literal-Typ — und der Compiler nutzt diese Discriminant-Property, um in switch- und if-Blöcken automatisch die richtige Variante einzuengen.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2; // s: { kind: "circle"; ... }
case "square":
return s.side ** 2; // s: { kind: "square"; ... }
case "rectangle":
return s.width * s.height; // s: { kind: "rectangle"; ... }
}
}Die kind-Property ist hier ein String-Literal-Typ pro Variante. Im switch engt TypeScript anhand des konkreten Strings den Typ von s exakt auf die passende Variante ein — ohne instanceof, ohne Type Guard-Funktion, ohne Cast.
| Variante | Wann verwenden |
|---|---|
enum Kind { Circle, ... } | Kompatibilität mit JS-Reflektion, numerische Werte gewünscht |
type Kind = "circle" | ... | Standard für neue Codebasen, kein Runtime-Code |
const Kind = { Circle: "circle" } as const | Wenn Werte als Konstanten zur Laufzeit gebraucht werden |
Praxis: API-Status, Routen, Theme-Keys
Drei typische Einsatzfelder im Alltag:
// API-Status — kompakter, lesbarer Zustands-Automat
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User[] }
| { status: "error"; error: Error };
// Routen — typsicher zentralisiert
const Routes = {
home: "/",
about: "/about",
contact: "/contact",
} as const;
type Route = typeof Routes[keyof typeof Routes];
// "/" | "/about" | "/contact"
// Theme-Keys — Design-Tokens als Literal-Union
type ColorToken =
| "background" | "surface" | "primary"
| "text" | "textMute" | "accent";
function color(token: ColorToken): string {
return `var(--color-${token})`;
}Das Routes-Pattern mit as const plus typeof plus indexierten Zugriff ist eine der häufigsten Anwendungen überhaupt: Eine schreibbare Konstante zur Laufzeit, automatisch abgeleitete Literal-Union zur Compile-Zeit, kein doppeltes Pflegen.
Besonderheiten
Objekt-Properties widen — auch innerhalb von const.
const obj = { status: "ok" } hat den Typ { status: string }, nicht { status: "ok" }. Der Grund: obj.status ist auch in einem const-Objekt mutierbar. Mit as const hinten dran wird der Typ exakt: { readonly status: "ok" }. Genau das ist die häufigste Quelle für „Type 'string' is not assignable to type '...'"-Fehler beim Aufruf von Funktionen mit Literal-Union-Parametern.
Function-Return-Type widet bei impliziter Inferenz.
Schreibst du function getMethod() { return "GET"; }, ist der Rückgabetyp string — nicht "GET". TypeScript widet bei Rückgabewerten, weil die Funktion potenziell verschiedene Strings liefern könnte. Mit explizitem Rückgabetyp : "GET" oder einer as const-Assertion am Return-Statement bleibt der Literal-Typ erhalten.
as const macht Strukturen tief-readonly.
Ein Objekt mit as const ist nicht nur auf der obersten Ebene readonly — der Modifier wirkt rekursiv durch alle verschachtelten Objekte und Arrays. Das ist mächtig für unveränderliche Konfigurations-Konstanten, aber inkompatibel mit Funktionen, die ein mutierbares Argument erwarten. Wer das umgehen will, klont vor der Übergabe oder typisiert die Funktion auf ReadonlyArray/Readonly<T> um.
Literal Union ersetzt enum praktisch immer.
type Kind = "a" | "b" ist gegenüber enum Kind { A, B } fast immer die bessere Wahl: kein Runtime-Code, keine doppelte Repräsentation (Name vs. Wert), volle Interop mit JSON und externen APIs, automatische String-Schlüssel ohne Mapping. Enums lohnen sich nur, wenn du eine echte Laufzeit-Repräsentation brauchst — etwa für Reflexion oder bitweise Flags.
Template Literal Types sind komposable Literal Types.
ist selbst ein Literal-Typ und lässt sich beliebig mit anderen Literal-Typen kombinieren. Damit baust du Typ-Schemata für CSS-Längen (hello ${string}), Event-Namen (${number}px), Routen (on${Capitalize<Event>}) oder API-Pfade. Die Inferenz funktioniert dabei in beide Richtungen — TypeScript kann Template Literal Types matchen und Teilstrings extrahieren./users/${string}
satisfies (seit 4.9) hält Werte schmal, ohne as const zu erzwingen.
Eine Type-Annotation wie : Record<Color, string> widet die Property-Werte auf string. as const erzwingt readonly überall. Das neuere satisfies validiert nur die Form, lässt aber die schmalen Literal-Typen der Werte stehen — perfekt für getypte Konfigurations-Objekte, deren Schlüssel-Werte später noch verengt referenziert werden sollen. Tabelle: Annotation prüft + widet, as const friert + verengt, satisfies prüft + erhält.
as vs. as const — unterschiedliche Semantik.
x as T ist eine Type Assertion: Du behauptest gegenüber dem Compiler, der Wert sei vom Typ T. Das ist potenziell unsicher und kann lügen. x as const ist eine Const Assertion: Sie weist den Compiler an, alle Subtypen zu Literal-Typen zu verengen und readonly zu setzen. Das ist immer sicher, weil es nur die Inferenz steuert, nicht eine fremde Behauptung in den Typ-Graph einschleust.
Literal-Inference funktioniert in generischen Constraints.
Eine Funktion function pick<K extends string>(key: K): K behält den exakten Literal-Typ des übergebenen Strings als K — vorausgesetzt, der Aufrufer übergibt einen Literal. Wird const k: string = "a"; pick(k) aufgerufen, widet K wieder zu string. Wer das verhindern will, nutzt entweder as const am Aufruf oder einen engeren Constraint wie K extends "a" | "b".
Weiterführende Ressourcen
Externe Quellen
- Literal Types – TypeScript Handbook
- TypeScript 4.9 Release Notes (satisfies)
- Template Literal Types – TypeScript Handbook