Type Level Programming Grundlagen in TypeScript
AI generated
<T>
type
TypeScript · Typsystem · Advanced Types
Type Level Programming Grundlagen
wie TypeScripts Typsystem zur eigenen funktionalen Sprache wird

Wer TypeScripts Typsystem nur als Annotation für Laufzeitwerte betrachtet, übersieht seine eigentliche Mächtigkeit. Conditional Types entsprechen If/Else, Mapped Types entsprechen ForEach, infer entspricht Pattern Matching, und Rekursion ist das einzige Kontrollflusskonstrukt. Zusammen bilden diese vier Bausteine eine vollständige funktionale Sprache, die zur Compile-Zeit ausgewertet wird und mit der sich sogar kleine Parser bauen lassen.

18 Min. Lesezeit Conditional Types · Mapped Types · infer · Rekursion TypeScript 4.x · 5.x

1. Was Type Level Programming bedeutet

Type Level Programming bezeichnet das Schreiben von Logik, die vollständig innerhalb des Typsystems ausgeführt wird, ohne dass zur Laufzeit ein einziges Byte Code dafür existiert. Statt Werte zu berechnen, berechnet man Typen aus anderen Typen, mit denselben Grundkonzepten wie in einer klassischen Programmiersprache: Verzweigung, Iteration, Destrukturierung und Rekursion, nur eben auf der Ebene von Typen statt Werten.

Für viele TypeScript-Entwickler bleibt Type Level Programming unsichtbar, weil die alltägliche Nutzung von Generics selten über einfache Parametrisierung hinausgeht. Sobald man aber Utility-Typen wie Partial, Pick oder ReturnType selbst nachbaut, betritt man diese zweite, parallele Sprachebene, die TypeScript neben der eigentlichen JavaScript-Laufzeit anbietet.

Dieser Artikel ordnet Type Level Programming systematisch ein: Welche vier Bausteine gibt es, wie verhalten sie sich zu bekannten Programmierkonzepten, und wo liegen die praktischen und theoretischen Grenzen dieser zur Compile-Zeit ausgewerteten Sprache innerhalb der Sprache.

2. Das Typsystem als eigene funktionale Sprache begreifen

TypeScripts Typsystem hat, historisch eher zufällig als geplant, alle Eigenschaften einer funktionalen Programmiersprache angesammelt. Es gibt Werte, in diesem Fall Typen, es gibt Funktionen über diese Werte, in diesem Fall generische Typen und Utility-Typen, und es gibt Kontrollstrukturen für Verzweigung und Wiederholung. Der entscheidende Unterschied zu einer gewöhnlichen funktionalen Sprache: Es gibt keine Seiteneffekte, keine veränderlichen Variablen und keine Schleifen im klassischen Sinn, nur Rekursion.

Diese Eigenschaften machen Type Level Programming strukturell näher zu Sprachen wie Haskell oder Prolog als zu imperativem TypeScript-Code selbst. Wer bereits funktionale Programmierung kennt, erkennt in Conditional Types sofort Pattern-Matching-Ausdrücke und in Mapped Types sofort List-Comprehensions. Wer diese Analogie noch nicht kennt, profitiert davon, sie explizit zu lernen, weil viele auf den ersten Blick kryptische Typdefinitionen dadurch plötzlich lesbar werden.

3. Baustein Conditional Types: If/Else auf Typebene

Ein Conditional Type der Form T extends U ? X : Y ist die direkte Entsprechung einer If/Else-Verzweigung, nur dass die Bedingung eine Typkompatibilitätsprüfung ist statt eines booleschen Laufzeitwerts. Diese Prüfung fragt: Ist jeder Wert vom Typ T auch ein gültiger Wert vom Typ U? Ist das der Fall, wertet der Ausdruck zu X aus, sonst zu Y.

Verkettete Conditional Types entsprechen dabei einer If/Else-If-Kette, wie man sie aus jeder imperativen Sprache kennt. Der wichtige Unterschied zu Laufzeit-Verzweigungen: Die Auswertung passiert vollständig zur Compile-Zeit, das Ergebnis ist bereits fest im erzeugten Typ verankert, es gibt keine Möglichkeit, zur Laufzeit einen anderen Zweig zu erreichen, als der Compiler ihn bereits bestimmt hat.


// A conditional type is an if/else expression evaluated at compile time
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type A = TypeName<string>;   // "string"
type B = TypeName<() => void>; // "function"

4. Baustein Mapped Types: ForEach über Eigenschaften

Ein Mapped Type der Form { [K in keyof T]: ... } entspricht einer ForEach-Schleife oder, funktional gedacht, einer List-Comprehension über die Schlüssel eines Objekttyps. Für jeden Schlüssel K aus keyof T wird ein neuer Eintrag im Ergebnistyp erzeugt, dessen Wert typischerweise von T[K] abhängt. Modifikatoren wie readonly und ? lassen sich dabei gezielt hinzufügen oder mit -readonly und -? gezielt entfernen.

Seit TypeScript 4.1 erlaubt die as-Klausel in Mapped Types zusätzlich, den Schlüssel selbst umzubenennen, etwa { [K in keyof T as `get${Capitalize}`]: () => T[K] }. Das entspricht funktional einer map-Operation, die nicht nur die Werte, sondern auch die Schlüssel einer Struktur transformiert, ein Muster, das man aus Object.fromEntries in JavaScript zur Laufzeit kennt, hier aber vollständig auf Typebene.


interface User {
  id: number;
  name: string;
  email: string;
}

// "ForEach" over keys: generate a getter method name per property
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }

5. Baustein infer: Pattern Matching und Destrukturierung

Das Schlüsselwort infer innerhalb eines Conditional Types entspricht Pattern Matching mit Destrukturierung, wie man es aus Sprachen wie Haskell oder Rust kennt. Statt eine Bedingung nur zu prüfen, extrahiert infer gleichzeitig einen Teil der Struktur als neue, benannte Typvariable. T extends Promise<infer U> ? U : T prüft nicht nur, ob T ein Promise ist, sondern bindet den enthaltenen Typ direkt an U, verfügbar im true-Zweig des Ausdrucks.

Dieses Muster funktioniert für beliebig komplexe Strukturen: Funktionssignaturen, Tupel, Template Literal Strings und verschachtelte generische Typen lassen sich alle über infer in ihre Bestandteile zerlegen. Genau diese Fähigkeit macht infer zum mächtigsten einzelnen Werkzeug im Type Level Programming, weil sie Prüfung und Extraktion in einem einzigen Ausdruck vereint, statt beide Schritte getrennt zu formulieren.


// Pattern matching on a function signature: extract the first parameter's type
type FirstParam<F> = F extends (first: infer P, ...rest: unknown[]) => unknown
  ? P
  : never;

function greet(name: string, times: number): void {}
type P = FirstParam<typeof greet>; // string

// Pattern matching on a Promise: unwrap the resolved type
type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;
type Resolved = Awaited2<Promise<Promise<number>>>; // number

6. Rekursion als einziges Kontrollflusskonstrukt

Während imperative Sprachen for- und while-Schleifen anbieten, kennt Type Level Programming nur Rekursion als Mittel für wiederholte Berechnung. Ein Conditional Type, der sich selbst mit einem strukturell kleineren Argument aufruft, übernimmt exakt die Rolle einer Schleife, mit dem Basisfall als Abbruchbedingung anstelle einer Schleifenbedingung. Diese Einschränkung ist kein Zufall, sondern folgt direkt aus dem fehlenden veränderlichen Zustand im Typsystem, ohne den eine klassische Schleife keinen Sinn ergäbe.

In der Praxis bedeutet das: Jede Aufgabe, die man in einer imperativen Sprache mit einer Schleife lösen würde, muss man im Typsystem als rekursive Struktur ausdrücken, mit den bereits diskutierten Grenzen bei der Rekursionstiefe. Ein Zähler-Tupel als Ersatz für eine Schleifenvariable, wie es viele Type-Level-Algorithmen nutzen, zeigt genau diese notwendige Übersetzung von imperativem in rekursives Denken.


// Recursion replaces iteration: no "for" loop exists at the type level
type Repeat<S extends string, N extends number, Acc extends string = ""> =
  Acc["length"] extends N ? Acc : Repeat<S, N, `${Acc}${S}`>;

type Line = Repeat<"-", 5>; // "-----"

7. Praxisbeispiel: Ein kleiner Type-Level-Parser für Routen

Die vier Bausteine, Conditional Types, Mapped Types, infer und Rekursion, lassen sich zu einem kleinen, aber praktisch nützlichen Type-Level-Parser kombinieren: einem Typ, der aus einer Route wie /users/:id/posts/:postId automatisch ein Objekt mit den enthaltenen Parametern ableitet. Frameworks wie Next.js oder tRPC nutzen exakt diese Technik, um Routenparameter typsicher verfügbar zu machen, ohne dass Entwickler sie manuell in einem separaten Interface pflegen müssen.

Der Parser zerlegt den Route-String rekursiv am Zeichen /, prüft mit einem Conditional Type, ob das aktuelle Segment mit : beginnt, und sammelt gefundene Parameter über infer in einem wachsenden Objekttyp. Dieses Beispiel zeigt anschaulich, wie alle bisher erklärten Konzepte in einer einzigen, praxisrelevanten Typdefinition zusammenwirken.


type ExtractRouteParams<Route extends string> =
  Route extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<Rest>]: string } // recurse on the remaining segments
    : Route extends `${string}:${infer Param}`
      ? { [K in Param]: string } // base case: trailing parameter, no more segments
      : {}; // base case: no parameters left

type Params = ExtractRouteParams<"/users/:id/posts/:postId">;
// { id: string; postId: string }

function buildUrl<R extends string>(route: R, params: ExtractRouteParams<R>): string {
  return Object.entries(params).reduce(
    (url, [key, value]) => url.replace(`:${key}`, String(value)),
    route as string
  );
}

buildUrl("/users/:id/posts/:postId", { id: "42", postId: "7" });

8. Grenzen: Turing-Vollständigkeit und Performance-Kosten

TypeScripts Typsystem ist seit einigen Jahren nachweislich Turing-vollständig, was bedeutet, dass sich theoretisch jedes berechenbare Problem als Typdefinition ausdrücken lässt, von einem Sudoku-Löser bis zu einem einfachen Interpreter für eine Programmiersprache. Diese theoretische Mächtigkeit ist aber nicht gleichbedeutend mit praktischer Eignung, weil der TypeScript-Compiler nicht für allgemeine Berechnung optimiert ist, sondern für Typprüfung in überschaubarer Zeit.

Jede zusätzliche Rekursionsebene, jeder zusätzliche Conditional Type und jeder zusätzliche Mapped Type kostet reale Compile-Zeit, die sich in großen Projekten mit vielen solchen Konstrukten spürbar summiert. Type Level Programming sollte deshalb gezielt für Probleme eingesetzt werden, bei denen echte Typsicherheit einen messbaren Wert hat, etwa bei API-Verträgen oder Routenparametern, nicht als generelles Werkzeug für jede denkbare Typtransformation.

9. Type-Level vs. Runtime Validation im Vergleich

Nicht jede Validierungsaufgabe gehört ins Typsystem. Die folgende Tabelle stellt gegenüber, wann Type-Level-Lösungen sinnvoll sind und wann Runtime-Validierung, etwa mit Zod, die bessere Wahl bleibt.

Aufgabe Type-Level Programming Runtime Validation Empfehlung
Statisch bekannte Routenparameter Sehr geeignet Unnötiger Overhead Type-Level-Parser wie in Abschnitt 7
Nutzereingaben aus Formularen Nicht möglich Erforderlich Zod oder ähnliche Runtime-Validierung
API-Antworten aus OpenAPI-Spec Für Typen aus Codegen Für tatsächliche Antwortprüfung Beide kombinieren
Interne Utility-Typen Sehr geeignet Nicht anwendbar DeepPartial, PickByType und Ähnliches

Die Faustregel: Type Level Programming eignet sich für Strukturen, die bereits zur Compile-Zeit vollständig bekannt sind, etwa Routen-Strings, Konfigurationsschlüssel oder API-Schemas aus generiertem Code. Sobald Daten zur Laufzeit von außen kommen, etwa aus einem Formular oder einer externen API-Antwort, ist Runtime-Validierung zwingend erforderlich, weil das Typsystem zur Laufzeit vollständig verschwindet.

Mironsoft

TypeScript-Architektur, Typsystem-Beratung und Refactoring

Typen, die eure API-Verträge wirklich absichern?

Wir bauen Type-Level-Parser für Routen und Konfigurationen, kombinieren sie mit sinnvoller Runtime-Validierung und schulen eure Teams im praktischen Umgang mit fortgeschrittenem Typsystem-Design.

Type-Level-Tooling

Route-Parser, Config-Typen und andere Compile-Zeit-Utilities für euer Projekt

Performance-Audit

Analyse, wo Type-Level-Konstrukte die Compile-Zeit unnötig belasten

Team-Schulung

Workshops zu Conditional Types, Mapped Types und infer für erfahrene Teams

10. Zusammenfassung

Type Level Programming begreift TypeScripts Typsystem als eigene, vollständig zur Compile-Zeit ausgewertete Sprache, mit direkten Entsprechungen zu bekannten Programmierkonzepten: Conditional Types als If/Else, Mapped Types als ForEach, infer als Pattern Matching mit Destrukturierung und Rekursion als einziges verfügbares Kontrollflusskonstrukt. Diese vier Bausteine lassen sich zu praktisch nützlichen Konstrukten wie einem Type-Level-Parser für Routenparameter kombinieren.

Die theoretische Turing-Vollständigkeit des Typsystems bedeutet nicht, dass jede Aufgabe dorthin gehört. Type Level Programming lohnt sich für Strukturen, die bereits zur Compile-Zeit feststehen, während Nutzereingaben und externe Daten weiterhin Runtime-Validierung brauchen. Wer diese Grenze kennt, nutzt das Typsystem als starkes, aber gezieltes Werkzeug statt als Selbstzweck.

Type Level Programming Grundlagen — Das Wichtigste auf einen Blick

If/Else

Conditional Types T extends U ? X : Y entsprechen Verzweigungen, ausgewertet zur Compile-Zeit statt zur Laufzeit.

ForEach

Mapped Types { [K in keyof T]: ... } iterieren über Schlüssel, seit TypeScript 4.1 auch mit Umbenennung über as.

Pattern Matching

infer prüft und extrahiert Teilstrukturen in einem Ausdruck, das mächtigste einzelne Werkzeug im Type Level Programming.

Grenzen

Turing-vollständig, aber praktisch begrenzt durch Compile-Zeit-Kosten. Nur für compile-zeit-bekannte Strukturen einsetzen.

11. FAQ: Type Level Programming Grundlagen

1Was ist Type Level Programming?
Logik, die vollständig im Typsystem ausgeführt wird, ohne Laufzeit-Code. Typen werden aus Typen mit Verzweigung, Iteration und Rekursion berechnet.
2Welche vier Bausteine gibt es?
Conditional Types als If/Else, Mapped Types als ForEach, infer als Pattern Matching, Rekursion als Kontrollfluss.
3Warum keine Schleifen?
Kein veränderlicher Zustand im Typsystem, deshalb übernimmt Rekursion mit Basisfall die Rolle der Wiederholung.
4Wirklich Turing-vollständig?
Ja, nachgewiesen. Theoretisch jedes berechenbare Problem ausdrückbar, praktisch aber durch Compile-Zeit begrenzt.
5Wie funktioniert ein Route-Parser?
Rekursive Zerlegung am /, infer prüft auf : am Segmentanfang, gefundene Parameter sammeln sich in einem Mapped Type.
6Wann statt Runtime-Validierung nutzen?
Wenn die Struktur schon zur Compile-Zeit feststeht. Für externe Daten bleibt Runtime-Validierung Pflicht.
7Was kostet es in der Praxis?
Jede Rekursionsebene und jeder Conditional Type erhöht die Compile-Zeit, in großen Projekten spürbar.
8infer vs. normale Prüfung?
infer bindet zusätzlich einen Teil der geprüften Struktur an eine neue Typvariable, nicht nur eine boolesche Bestätigung.
9Schlüssel umbenennen mit Mapped Types?
Ja, seit TypeScript 4.1 über die as-Klausel, entspricht map über Schlüssel und Werte gleichzeitig.
10FP-Kenntnisse nötig?
Nicht zwingend, aber hilfreich. Die Konzepte entsprechen direkt Pattern Matching und List Comprehensions aus funktionalen Sprachen.