Von String-Literalen zu typsicheren APIs
Template Literal Types kombinieren String-Literal-Typen zu neuen Literal-Typen und machen aus laxen string-Parametern präzise, überprüfbare Vertragsformen. Event-Namen, CSS-Klassenkombinationen, Routen-Pfade und Konfigurationsschlüssel lassen sich damit bereits beim Kompilieren validieren, lange bevor ein falscher String zur Laufzeit einen stillen Bug erzeugt oder ein Kunde einen kaputten Link anklickt.
Inhaltsverzeichnis
- 1. Template Literal Types: Grundlagen
- 2. Union-Kombinationen: die kombinatorische Kraft von Templates
- 3. Typisierte Event-Namen mit Capitalize
- 4. Typisierte CSS-Klassenkombinationen
- 5. Typisierte Routen-Pfade
- 6. Generics kombinieren: typsichere String-Builder
- 7. Intrinsische String-Manipulationstypen
- 8. Inferenz mit infer in Template Literal Types
- 9. Template Literal Types im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Template Literal Types: Grundlagen
Template Literal Types nutzen seit TypeScript 4.1 dieselbe Backtick-Syntax wie normale Template-Strings, werden aber zur Kompilierzeit statt zur Laufzeit ausgewertet. Aus type Name = "Mira" | "Timo" und einem Muster wie `Hello, ${Name}!` entsteht ein neuer Literal-Typ, der jede Kombination aus Präfix, eingesetztem Literal und Suffix als eigenständiges Mitglied einer Union auflistet. Der Compiler kennt damit nicht nur, dass ein Wert ein string ist, sondern welche konkreten Zeichenketten überhaupt gültig sind.
Der Unterschied zu einem einfachen string-Parameter ist der entscheidende Vorteil: Ein string akzeptiert jede beliebige Zeichenkette, auch Tippfehler, veraltete Event-Namen oder falsch zusammengesetzte Routen. Ein Template Literal Type grenzt den gültigen Wertebereich exakt ein und lässt den Compiler bereits beim Schreiben des Codes reagieren, nicht erst beim Testen oder in Produktion.
2. Union-Kombinationen: die kombinatorische Kraft von Templates
Werden mehrere Union-Typen in ein Template Literal Type eingesetzt, bildet der Compiler automatisch das kartesische Produkt aller Kombinationen. Aus zwei Unions mit je drei Mitgliedern entstehen neun konkrete String-Literale, aus drei Unions mit je vier Mitgliedern bereits 64. Diese kombinatorische Explosion ist meistens erwünscht, weil sie jede gültige Kombination exakt und ohne manuelle Pflege einer Liste abdeckt, etwa bei Sprachcodes, Konfigurationsschlüsseln oder CSS-Klassennamen.
Praktisch bedeutet das: Statt type LocaleTag = string zu deklarieren und Tippfehler wie "de_DE" statt "de-DE" erst zur Laufzeit zu bemerken, generiert TypeScript die vollständige, korrekt geschriebene Union automatisch aus den Bausteinen. Bei sehr großen Unions mit mehreren tausend Kombinationen sollte man die Typprüfzeit im Auge behalten, da der Compiler jede Kombination einzeln materialisiert.
// Template literal types combine literal types into new literal unions
type Locale = "de" | "en" | "fr";
type Region = "DE" | "US" | "FR";
// Cross product: every Locale combined with every Region
type LocaleTag = `${Locale}-${Region}`;
// "de-DE" | "de-US" | "de-FR" | "en-DE" | "en-US" | "en-FR" | "fr-DE" | "fr-US" | "fr-FR"
type Greeting = `Hello, ${string}!`;
const greet = (message: Greeting): void => {
console.log(message);
};
greet("Hello, World!"); // OK
// greet("Hi, World!"); // Error: does not match the pattern
3. Typisierte Event-Namen mit Capitalize
Ein klassisches Einsatzgebiet für Template Literal Types sind Event-Handler-Props nach dem Muster onClick, onChange oder onSubmit. Statt jeden Handler-Namen einzeln als String-Literal zu pflegen, leitet man ihn aus der Event-Union mit on${Capitalize<EventName>} ab. Der intrinsische Typ Capitalize setzt den ersten Buchstaben jedes Union-Mitglieds groß, das Template davor fügt das Präfix on hinzu, aus "click" | "focus" | "change" wird automatisch "onClick" | "onFocus" | "onChange".
In Kombination mit einem Mapped Type wie { [K in EventHandlerName]?: (event: Event) => void } entsteht eine Props-Definition, bei der jeder Tippfehler im Handler-Namen sofort als Compile-Fehler auffällt: onClik statt onClick wird nicht mehr erst im Browser sichtbar. Diese Ableitung hält Event-Union und Handler-Signatur an genau einer einzigen Stelle synchron.
// Typed event names using Capitalize<T> and template literals
type EventName = "click" | "focus" | "change" | "submit";
// Build handler prop names like onClick, onFocus, onChange, onSubmit
type EventHandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onChange" | "onSubmit"
type EventHandlers = {
[K in EventHandlerName]?: (event: Event) => void;
};
const handlers: EventHandlers = {
onClick: (event) => console.log("clicked", event.target),
onChange: (event) => console.log("changed", event.target),
// onClik: () => {}, // Error: not assignable, typo caught at compile time
};
// Generic helper: derive the handler prop name from any event union member
function makeHandlerName<E extends string>(event: E): `on${Capitalize<E>}` {
return `on${event.charAt(0).toUpperCase()}${event.slice(1)}` as `on${Capitalize<E>}`;
}
4. Typisierte CSS-Klassenkombinationen
In komponentenbasierten Design-Systemen, wie sie auch in Hyvä-Themes mit Tailwind CSS entstehen, folgen CSS-Klassennamen häufig einem festen Schema aus Variante, Größe und optionalem Zustand, etwa btn-primary-md oder btn-danger-lg-hover. Ein Template Literal Type aus den zugrunde liegenden Union-Typen Variant, Size und State bildet exakt die Menge gültiger Klassennamen ab und verhindert, dass eine Funktion eine nicht existierende Kombination wie btn-primary-xl zurückgibt.
Der Wert dieses Patterns liegt vor allem in Funktionen, die Klassennamen dynamisch zusammensetzen: Der Rückgabetyp dokumentiert selbst, welche Klassen überhaupt entstehen können, und jede Refaktorierung der zugrunde liegenden Unions aktualisiert automatisch alle abhängigen Stellen. Für vollständig aus Nutzereingaben zusammengebaute Klassenstrings bleibt die statische Prüfung naturgemäß wirkungslos, das Pattern eignet sich für kontrollierte, endliche Kombinationen, nicht für beliebigen Freitext.
// Typed CSS class combinations for a design-system button component
type Size = "sm" | "md" | "lg";
type Variant = "primary" | "secondary" | "danger";
type State = "default" | "hover" | "disabled";
type ButtonClass =
| `btn-${Variant}-${Size}`
| `btn-${Variant}-${Size}-${Exclude<State, "default">}`;
function buttonClass(variant: Variant, size: Size, state: State = "default"): ButtonClass {
return state === "default"
? `btn-${variant}-${size}`
: `btn-${variant}-${size}-${state}`;
}
buttonClass("primary", "md"); // "btn-primary-md"
buttonClass("danger", "lg", "hover"); // "btn-danger-lg-hover"
// buttonClass("primary", "xl"); // Error: "xl" is not assignable to Size
5. Typisierte Routen-Pfade
Routen-Definitionen sind ein weiteres Feld, in dem string als Parametertyp zu grobkörnig ist. Ein Pfad wie /users/${string}/posts/${number} beschreibt nicht nur, dass eine Zeichenkette erwartet wird, sondern dass exakt zwei Segmente an festen Positionen folgen müssen und das zweite numerisch sein soll. Eine Navigationsfunktion, die diesen Typ als Parameter nutzt, lehnt /users/mira/posts/abc bereits beim Kompilieren ab, weil abc keinem ${number}-Segment entspricht.
Mit dem infer-Schlüsselwort innerhalb bedingter Typen lässt sich der umgekehrte Weg gehen: Aus einem Routen-Muster wie /users/:userId/posts/:postId extrahiert ein rekursiver Typ automatisch ein Objekt mit den Parameternamen als Schlüssel. Das ist die Grundlage typsicherer Router-Bibliotheken, bei denen useParams() exakt die Felder liefert, die im jeweiligen Routen-String tatsächlich vorkommen, ohne manuell gepflegte Parameter-Interfaces, die mit der Zeit vom echten Pfad abweichen.
// Typed route paths with parameter extraction via infer
type RoutePath = `/users/${string}/posts/${number}` | `/orders/${string}`;
function navigate(path: RoutePath): void {
window.history.pushState({}, "", path);
}
navigate("/users/mironsoft/posts/42"); // OK
// navigate("/users/mironsoft/posts/abc"); // Error: "abc" is not a number segment
// Extract typed params from a route pattern at the type level
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<Rest>]: string }
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: Record<string, never>;
type UserPostParams = ExtractParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }
6. Generics kombinieren: typsichere String-Builder
Template Literal Types entfalten ihre volle Stärke erst in Kombination mit Generics. Eine generische Typfunktion wie EnvKey<Prefix, Name> nimmt zwei generische String-Parameter entgegen und baut daraus über ${Uppercase<Prefix>}_${Uppercase<Name>} einen neuen, präzisen Literal-Typ. Der Rückgabetyp ist nicht länger string, sondern zum Beispiel exakt "APP_DB_HOST", abhängig von den konkret übergebenen Literalen.
Dieses Pattern eignet sich hervorragend für Build-Skripte, Konfigurationsobjekte und typsichere Wrapper um Umgebungsvariablen oder API-Endpunkte: Eine Funktion readEnv(prefix, name) gibt nicht nur einen Wert, sondern einen Typ zurück, der exakt beschreibt, welcher Schlüssel gelesen wurde. Voraussetzung ist, dass die generischen Parameter eng genug typisiert sind, etwa mit extends string, sonst weitet TypeScript sie beim Aufruf zu string und der Präzisionsgewinn geht verloren.
// Intrinsic string manipulation types combined with a generic builder
type EnvKey<Prefix extends string, Name extends string> =
`${Uppercase<Prefix>}_${Uppercase<Name>}`;
type DbEnvKey = EnvKey<"app", "db_host">; // "APP_DB_HOST"
function readEnv<Prefix extends string, Name extends string>(
prefix: Prefix,
name: Name,
): EnvKey<Prefix, Name> {
return `${prefix.toUpperCase()}_${name.toUpperCase()}` as EnvKey<Prefix, Name>;
}
// Uncapitalize / Capitalize round trip for accessor names
type Getter<Field extends string> = `get${Capitalize<Field>}`;
type Setter<Field extends string> = `set${Capitalize<Field>}`;
type FieldFromGetter<G extends string> =
G extends `get${infer Field}` ? Uncapitalize<Field> : never;
type PriceField = FieldFromGetter<"getPrice">; // "price"
class Model<Field extends string> {
constructor(private data: Record<Field, unknown>) {}
}
7. Intrinsische String-Manipulationstypen
TypeScript liefert vier intrinsische Typen speziell für die Typebene: Uppercase<T> und Lowercase<T> wandeln einen String-Literal-Typ vollständig in Groß- oder Kleinschreibung um, Capitalize<T> setzt nur den ersten Buchstaben groß, Uncapitalize<T> nur den ersten Buchstaben klein. Anders als eigene bedingte Typen sind diese vier direkt im Compiler implementiert, weil sich Zeichen-für-Zeichen-Transformationen im regulären Typsystem nicht rekursiv sauber ausdrücken lassen.
Kombiniert mit Template Literal Types entstehen daraus symmetrische Paare wie Getter<Field> = `get${Capitalize<Field>}` und die Rückrichtung FieldFromGetter<G>, die per infer und Uncapitalize aus "getPrice" wieder "price" gewinnt. Solche Paare sind die Grundlage für automatisch generierte Getter/Setter-Signaturen in ORM-artigen Modellklassen, ohne dass Feldname und Methodenname jemals manuell synchron gehalten werden müssen.
8. Inferenz mit infer in Template Literal Types
Das Schlüsselwort infer innerhalb eines bedingten Typs erlaubt es, Teile eines String-Literal-Typs an einem Template-Muster zu binden und als eigenständige Typen weiterzuverwenden. T extends `${infer Head}/${infer Tail}` zerlegt beispielsweise einen Pfad am ersten Schrägstrich in Kopf- und Restsegment. Rekursive Anwendung dieses Musters, bei der der Typ sich selbst mit dem verbleibenden Tail aufruft, verarbeitet beliebig viele Segmente und bildet damit vollständige Parser auf Typebene ab, etwa zum Aufsplitten von Routen, CSV-Kopfzeilen oder SQL-Spaltenlisten.
Seit TypeScript 4.5 unterstützt der Compiler tail-rekursive bedingte Typen mit deutlich höherer Rekursionstiefe, bevor die Fehlermeldung Type instantiation is excessively deep auftritt. Trotzdem bleibt Vorsicht angebracht: Bei sehr langen Strings oder stark verschachtelten Templates steigt die Typprüfzeit spürbar, und der Compiler ist kein vollwertiger Parser, komplexe Grammatiken mit Backtracking gehören weiterhin in Laufzeit-Code, nicht ins Typsystem.
9. Template Literal Types im direkten Vergleich
Die folgende Übersicht zeigt für typische Szenarien, wie stark sich ein locker typisierter string-Parameter von der Präzision eines Template Literal Type unterscheidet, und welche Fehlerklassen jeweils erst zur Laufzeit beziehungsweise bereits beim Kompilieren auffallen.
| Szenario | Naiv (string) | Idiomatisch (Template Literal Type) | Vorteil |
|---|---|---|---|
| Event-Handler-Prop | onClick: string |
on${Capitalize<EventName>} |
Tippfehler im Handler-Namen wird abgelehnt |
| CSS-Klasse | className: string |
btn-${Variant}-${Size} |
Ungültige Klassenkombination unmöglich |
| Routen-Pfad | path: string |
/users/${string}/posts/${number} |
Falscher Segmenttyp wird abgelehnt |
| Umgebungsvariable | key: string |
EnvKey<Prefix, Name> |
Präfix/Name-Konvention wird erzwungen |
| HTTP-Methode + Pfad | request: string |
${HttpMethod} ${RoutePath} |
Ungültige Methode-Pfad-Kombination unmöglich |
In allen fünf Szenarien ist der Unterschied nicht kosmetisch: Ein naiver string-Parameter verschiebt die Fehlererkennung in Tests, Code-Reviews oder im schlimmsten Fall in die Produktion. Ein Template Literal Type verschiebt dieselbe Fehlererkennung an den frühestmöglichen Zeitpunkt, den Moment, in dem der Code geschrieben wird, direkt im Editor.
Mironsoft
TypeScript-Architektur, Type-Level Programming und Headless-Integrationen
TypeScript-Typen, die Fehler wirklich verhindern?
Wir analysieren bestehende TypeScript-Codebasen, ersetzen lose string-Typisierung durch Template Literal Types und bauen typsichere Build-Skripte, Router und API-Clients für euren Magento- und Hyvä-Frontend-Stack.
Type-Audit
Bestehende Typdefinitionen auf zu lockere string-Typisierung prüfen
Refactoring
Template Literal Types, Generics und intrinsische Typen gezielt einführen
Tooling
Typsichere Build-Skripte, Router und API-Clients für Headless-Setups
10. Zusammenfassung
Template Literal Types verwandeln den größten gemeinsamen Nenner aller Strings, den Typ string, in präzise, überprüfbare Literal-Typen. Aus Union-Typen wie Event-Namen, CSS-Varianten oder Routen-Segmenten entstehen über Backtick-Syntax neue Unions, die jede gültige Kombination exakt abbilden. Die intrinsischen Typen Uppercase, Lowercase, Capitalize und Uncapitalize ergänzen das System um Zeichen-Transformationen, die sich mit regulären bedingten Typen nicht ausdrücken ließen.
In Kombination mit Generics und infer werden Template Literal Types zur Grundlage typsicherer String-Builder, Router-Bibliotheken und Konfigurationssysteme. Der Aufwand lohnt sich überall dort, wo Strings nach einem festen Muster aufgebaut sind, und bleibt bewusst begrenzt auf endliche, kontrollierte Kombinationen, nicht auf beliebigen Freitext aus Nutzereingaben.
Template Literal Types für typsichere Strings, das Wichtigste auf einen Blick
Grundlagen
Backtick-Syntax auf Typebene: `Hello, ${Name}!` erzeugt aus Union-Typen präzise Literal-Typ-Unions statt lockerem string.
Events & CSS-Klassen
on${Capitalize<EventName>} und btn-${Variant}-${Size} fangen Tippfehler bereits beim Kompilieren ab.
Routen & infer
/users/${string}/posts/${number} plus infer extrahiert typisierte Parameter aus Routen-Mustern.
Intrinsische Typen
Uppercase, Lowercase, Capitalize, Uncapitalize, direkt im Compiler implementiert, für Zeichen-Transformationen.