Template Literal Types für typsichere Strings
<T>
type
TypeScript · Template Literal Types · Generics · Type Safety
Template Literal Types für typsichere Strings
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.

14 Min. Lesezeit Template Literal Types · Capitalize · Uppercase TypeScript 4.1+ · Type-Level Programming

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.

11. FAQ: Template Literal Types für typsichere Strings

1Was ist ein Template Literal Type in TypeScript?
Backtick-Syntax auf Typebene: Aus Präfix, eingesetzten Literal-Typen und Suffix entsteht ein neuer String-Literal-Typ oder eine Union daraus, ausgewertet zur Kompilierzeit.
2Ab welcher TypeScript-Version gibt es Template Literal Types?
Seit TypeScript 4.1, zusammen mit den intrinsischen Typen Uppercase, Lowercase, Capitalize und Uncapitalize.
3Was ist der Unterschied zwischen Capitalize und Uppercase?
Capitalize setzt nur den ersten Buchstaben groß, der Rest bleibt unverändert. Uppercase wandelt die gesamte Zeichenkette um. Uncapitalize und Lowercase sind die Gegenstücke.
4Wie entsteht aus mehreren Union-Typen ein Template Literal Type?
Der Compiler bildet automatisch das kartesische Produkt aller eingesetzten Union-Mitglieder als neue Union von String-Literalen.
5Kann ich Routen-Parameter typsicher extrahieren?
Ja, mit infer in einem bedingten Typ. Ein rekursiver Typ zerlegt ein Muster wie /users/:userId/posts/:postId in ein Objekt mit den Parameternamen als Schlüssel.
6Was macht infer in einem Template Literal Type?
infer bindet einen zum Template-Muster passenden Teilstring an einen neuen Typnamen, der im weiteren bedingten Typ verwendet werden kann.
7Wie viele Kombinationen sind maximal sinnvoll?
Kein fester Grenzwert, aber jede Kombination wird materialisiert. Bei mehreren tausend Kombinationen steigt die Typprüfzeit spürbar und Instantiation-Limits können erreicht werden.
8Sind Template Literal Types zur Laufzeit vorhanden?
Nein, sie existieren nur zur Kompilierzeit und werden beim Transpilieren zu JavaScript vollständig entfernt.
9Wie kombiniere ich Template Literal Types mit Generics?
Ein generischer Typ wie EnvKey<Prefix, Name> baut aus generischen String-Parametern über ${Uppercase<Prefix>}_${Uppercase<Name>} einen präzisen Rückgabetyp.
10Wann lieber bei string bleiben?
Bei vollständig aus Nutzereingaben stammendem Freitext ohne festes Muster. Template Literal Types lohnen sich für kontrollierte, endliche Kombinationen.