Typen aus anderen Typen extrahieren
Das infer Schlüsselwort ist der Schlüssel zu vielen TypeScript Utility Types und erlaubt es, Teiltypen wie das Resultat eines Promise oder die Parameter einer Funktion direkt aus einem bestehenden Typ zu extrahieren, ohne sie manuell zu duplizieren. Wer versteht, wie infer innerhalb conditional types arbeitet, kann eigene Utility Types bauen, Duplikate vermeiden und Typen automatisch synchron zu ihrer Quelle halten.
Inhaltsverzeichnis
- 1. Warum infer für TypeScript Entwickler unverzichtbar ist
- 2. Die Mechanik: Wie infer innerhalb conditional types funktioniert
- 3. Promise-Typen extrahieren: Das Resultat eines Promise erfassen
- 4. Funktionstypen extrahieren: Parameter und Rückgabewerte
- 5. Eigene ReturnType- und Awaited-Utilities selbst bauen
- 6. Mehrfaches infer und Union Distribution
- 7. Grenzen und Fallstricke von infer
- 8. Praxis: Formulare, API Clients und Event Systeme
- 9. infer im Vergleich: manuell vs. automatisch abgeleitet
- 10. Zusammenfassung
- 11. FAQ
1. Warum infer für TypeScript Entwickler unverzichtbar ist
Wer aus der Magento- und PHP-Welt kommt, kennt das Problem: Ein Rückgabetyp ändert sich in einer Methode, und an zehn anderen Stellen im Code muss der Aufrufer manuell nachziehen. TypeScript löst dieses Problem in der Theorie durch statische Typisierung, aber ohne infer tappt man in dieselbe Falle: Man dupliziert Typdefinitionen von Hand, weil es keinen Weg gibt, einen Teiltyp aus einem bestehenden Typ herauszuziehen. Das infer Schlüsselwort schließt genau diese Lücke.
Praktisch bedeutet das: Statt den Rückgabetyp einer Funktion, den aufgelösten Wert eines Promise oder die Parameter einer Callback-Signatur erneut zu tippen, lässt man TypeScript den Typ selbst ableiten. Das Ergebnis bleibt automatisch synchron mit der Quelle, der Compiler schlägt bei jeder inkompatiblen Änderung sofort Alarm, und Utility Types wie ReturnType, Parameters oder Awaited aus der TypeScript-Standardbibliothek basieren intern exakt auf diesem Mechanismus.
2. Die Mechanik: Wie infer innerhalb conditional types funktioniert
infer existiert ausschließlich innerhalb der extends-Klausel eines conditional type. Die allgemeine Form T extends U ? X : Y prüft, ob T strukturell zu U passt. Ersetzt man einen Teil von U durch infer Name, weist man TypeScript an, an genau dieser Position eine neue Typvariable zu deklarieren und sie mit dem Teil von T zu füllen, der dort strukturell passt. Diese Variable steht danach im true-Zweig (X) zur Verfügung, so wie eine Capture Group in einem regulären Ausdruck den passenden Teilstring merkt.
Wichtig ist die Reihenfolge: Der Compiler versucht zuerst, T gegen die Struktur mit dem infer-Platzhalter zu matchen. Schlägt das Pattern Matching fehl, greift der false-Zweig, und die inferierte Variable existiert dort nicht. Genau das macht infer zu einem strukturellen Werkzeug: Es beschreibt eine Form, keine konkrete Bedingung, und TypeScript löst die Unbekannte durch Unifikation gegen den tatsächlichen Typ auf.
// Basic conditional type using infer to capture a sub-type
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // never, boolean is not an array
// infer only works inside the "extends" clause of a conditional type
// T extends (infer U)[] ? U : never
// ^^^^^^^ captures the array element type as U
3. Promise-Typen extrahieren: Das Resultat eines Promise erfassen
Der klassische Einstiegsfall für infer ist das Extrahieren des aufgelösten Werts eines Promise. Ein async Client liefert oft Promise<SomeResponse> zurück, aber an vielen Stellen im Code braucht man nur SomeResponse, etwa um einen Parameter oder ein Feld in einem Formular zu typisieren. Mit T extends Promise<infer U> ? U : T lässt sich dieser innere Typ ohne manuelle Zusatzarbeit herausziehen, und die Definition bleibt korrekt, selbst wenn sich der Rückgabetyp der ursprünglichen Funktion später ändert.
Die naive Variante hat jedoch eine Schwäche: Verschachtelte Promises wie Promise<Promise<string>> werden nur einmal aufgelöst. Genau dieses Problem löst das eingebaute Awaited<T> Utility Type durch Rekursion, indem es sich innerhalb des true-Zweigs erneut selbst aufruft, bis kein Promise mehr übrig bleibt. Diese rekursive Anwendung von infer ist ein Muster, das in vielen fortgeschrittenen Utility Types wiederkehrt.
// Extract the resolved type of a Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<{ id: number }>>; // { id: number }
type C = Unwrap<number>; // number, not a promise, returned as-is
// Naive Unwrap does not resolve nested promises
type D = Unwrap<Promise<Promise<string>>>; // Promise<string>, still wrapped once
// Recursive version resolves nested promises fully, like the built-in Awaited<T>
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type E = DeepUnwrap<Promise<Promise<string>>>; // string
4. Funktionstypen extrahieren: Parameter und Rückgabewerte
Funktionssignaturen lassen sich mit infer ebenso gezielt zerlegen wie Promises. Die Struktur T extends (...args: infer P) => unknown ? P : never erfasst die komplette Parameterliste als Tupel, inklusive Namen und optionaler Parameter, weil TypeScript die Rest-Parameter-Syntax auf Typebene genauso versteht wie zur Laufzeit. Das ist besonders nützlich, wenn man einen Wrapper um eine bestehende Funktion baut und sicherstellen will, dass sich die Signatur des Wrappers automatisch anpasst, sobald sich die Originalfunktion ändert.
Man kann die Position des infer-Platzhalters gezielt verschieben, um nur einen Teil der Parameter zu erfassen, etwa nur den ersten Parameter über (first: infer F, ...rest: unknown[]) => unknown. Diese Präzision unterscheidet infer von einer reinen Kopie der Signatur: Man beschreibt exakt, welcher Ausschnitt der Struktur interessant ist, und überlässt den Rest der Struktur bewusst dem Platzhalter unknown.
// Extract the parameter tuple of a function type
type Params<T> = T extends (...args: infer P) => unknown ? P : never;
function createOrder(customerId: string, items: string[], priority: boolean) {
return { customerId, items, priority };
}
type CreateOrderParams = Params<typeof createOrder>;
// [customerId: string, items: string[], priority: boolean]
// Extract just the first parameter
type FirstParam<T> = T extends (first: infer F, ...rest: unknown[]) => unknown
? F
: never;
type FirstArg = FirstParam<typeof createOrder>; // string
5. Eigene ReturnType- und Awaited-Utilities selbst bauen
Am besten versteht man infer, indem man die eingebauten Utility Types einmal selbst nachbaut. Eine vereinfachte Version von ReturnType<T> braucht nur eine Constraint, die sicherstellt, dass T überhaupt eine Funktion ist, und einen conditional type, der den Rückgabetyp über infer R an der Position nach dem Pfeil erfasst. Der eingebaute Typ in lib.es5.d.ts unterscheidet sich kaum von dieser minimalen Implementierung, er ist im Kern genau dieses eine Pattern.
Für Awaited<T> kombiniert man infer mit Rekursion: Der conditional type prüft, ob der inferierte Typ U selbst wieder ein Promise ist, und ruft sich in diesem Fall erneut mit U auf, statt sofort zurückzugeben. Diese Selbstbezüglichkeit ist in TypeScript seit einigen Versionen explizit erlaubt und wird vom Compiler mit einer Rekursionstiefe begrenzt, um Endlosschleifen im Typsystem zu verhindern.
// A simplified version of the built-in ReturnType<T> utility
type MyReturnType<T extends (...args: never[]) => unknown> =
T extends (...args: never[]) => infer R ? R : never;
function fetchOrder(id: string) {
return { id, total: 42, currency: "EUR" };
}
type Order = MyReturnType<typeof fetchOrder>;
// { id: string; total: number; currency: string }
// A simplified version of the built-in Awaited<T> utility
type MyAwaited<T> = T extends Promise<infer U>
? U extends Promise<unknown>
? MyAwaited<U>
: U
: T;
async function fetchOrderAsync(id: string) {
return fetchOrder(id);
}
type AsyncOrder = MyAwaited<ReturnType<typeof fetchOrderAsync>>;
// { id: string; total: number; currency: string }
6. Mehrfaches infer und Union Distribution
Ein conditional type ist nicht auf ein einziges infer beschränkt. T extends (...args: infer P) => infer R ? [P, R] : never erfasst Parameter und Rückgabetyp in einem einzigen Durchlauf und liefert beide als Tupel zurück. Jede infer-Position wird unabhängig gegen die passende Stelle der Struktur von T aufgelöst, solange das gesamte Muster strukturell zu T passt. Das erlaubt kompakte Utility Types, die mehrere Informationen gleichzeitig aus einer einzigen Signatur ziehen, ohne den Typ zweimal zu durchlaufen.
Eine Besonderheit betrifft distributive conditional types: Steht ein nackter Typparameter direkt vor extends, verteilt TypeScript den conditional type automatisch über jedes Mitglied einer Union. Bei type Wrapped<T> = T extends Promise<infer U> ? U : T, angewendet auf Promise<string> | Promise<number>, wird infer also für jedes Union-Mitglied einzeln ausgewertet, und das Ergebnis ist die Union string | number, nicht ein einzelner unaufgelöster Typ.
7. Grenzen und Fallstricke von infer
infer ist strikt an den extends-Zweig eines conditional type gebunden und kann nirgendwo sonst stehen, weder in einer eigenständigen Typdeklaration noch in einem Interface. Der häufigste Anfängerfehler ist der Versuch, infer außerhalb dieses Kontexts zu verwenden, was TypeScript mit einem klaren Compilerfehler quittiert. Ein zweiter Stolperstein: Taucht dieselbe infer-Variable mehrfach in kontravarianter Position auf, etwa in mehreren Funktionsparametern, bildet der Compiler statt einer Union eine Intersection, was zu überraschend engen Typen führen kann.
Auch die Fehlerdiagnose ist anspruchsvoller als bei normalen Typfehlern: Passt die Struktur nicht, landet man häufig im false-Zweig und damit bei never, ohne eine aussagekräftige Fehlermeldung an der eigentlichen Ursache. Tief verschachtelte, rekursive infer-Konstruktionen können außerdem die Typprüfung spürbar verlangsamen und in Extremfällen die von TypeScript intern begrenzte Rekursionstiefe überschreiten. Ein guter Kompromiss ist, komplexe infer-Ketten in benannte Zwischenschritte aufzuteilen, statt alles in einem einzigen, kaum lesbaren Typausdruck unterzubringen.
8. Praxis: Formulare, API Clients und Event Systeme
In der Praxis taucht infer selten isoliert auf, sondern eingebettet in größere Utility Types produktiver Bibliotheken. Formular-Bibliotheken leiten den Typ eines Feldwerts aus einem Validierungsschema ab, typsichere API Clients extrahieren den Response-Typ direkt aus der Definition eines Endpunkts, und Event-Systeme erfassen den Payload-Typ eines Listeners, ohne ihn an der Aufrufstelle erneut aufzuschreiben. In allen drei Fällen verhindert infer, dass Laufzeitverhalten und Typdefinition auseinanderlaufen.
Ein typisches Beispiel ist ein typisierter Event Bus, bei dem der Payload-Typ eines Listeners automatisch aus dessen Funktionssignatur abgeleitet wird, statt ihn separat zu deklarieren. Registriert man einen Listener für ein bestimmtes Event, kennt TypeScript dank infer und Mapped Types bereits die exakte Form des Payloads, inklusive Autovervollständigung im Editor. Das reduziert nicht nur Tipparbeit, sondern verhindert eine ganze Klasse von Fehlern, bei denen Payload und Listener-Signatur stillschweigend auseinanderdriften.
// Practical example: a typed event bus derives payload types via infer
type EventMap = {
"order:created": { orderId: string; total: number };
"order:cancelled": { orderId: string; reason: string };
};
type Listener<T> = (payload: T) => void;
// Extract the payload type straight out of the listener signature
type PayloadOf<L> = L extends Listener<infer P> ? P : never;
class TypedEventBus<Events extends Record<string, unknown>> {
private listeners: { [K in keyof Events]?: Listener<Events[K]>[] } = {};
on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void {
(this.listeners[event] ??= []).push(listener);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.listeners[event]?.forEach((listener) => listener(payload));
}
}
const bus = new TypedEventBus<EventMap>();
bus.on("order:created", (payload) => {
// payload is inferred as { orderId: string; total: number }
console.log(payload.orderId);
});
9. infer im Vergleich: manuell vs. automatisch abgeleitet
Die folgende Übersicht stellt den manuellen Ansatz, bei dem Typen von Hand dupliziert werden, dem infer-basierten Ansatz gegenüber, bei dem TypeScript den Typ automatisch aus der Quelle ableitet. Der Unterschied wirkt auf den ersten Blick klein, hat aber direkten Einfluss auf die Wartbarkeit größerer Codebasen.
| Anwendungsfall | Manuell dupliziert | Mit infer abgeleitet | Vorteil |
|---|---|---|---|
| Promise-Resultat | type Result = User; | Awaited<ReturnType<typeof fn>> | Bleibt bei Signaturänderung korrekt |
| Rückgabetyp einer Funktion | type Return = Order; | ReturnType<typeof fn> | Kein Duplikat, immer aktuell |
| Funktionsparameter | type Args = [string, number]; | Parameters<typeof fn> | Erfasst auch optionale Parameter |
| Array-Elementtyp | type Item = Product; | T extends (infer U)[] ? U : never | Funktioniert generisch für jedes Array |
| Objekt-Wertetyp | type Value = string; | T extends Record<string, infer V> ? V : never | Passt sich bei neuen Feldern automatisch an |
In jeder Zeile der Tabelle gilt dasselbe Prinzip: Der manuelle Ansatz funktioniert nur so lange, bis sich die Quelle ändert und niemand daran denkt, die Kopie nachzuziehen. Der infer-basierte Ansatz bleibt dagegen automatisch korrekt, weil der abgeleitete Typ strukturell an seine Quelle gebunden ist und sich bei jeder Änderung von selbst aktualisiert.
Mironsoft
TypeScript Code-Review, Utility-Type-Refactoring und typsichere API-Clients für Magento- und Hyvä-Projekte
Bereit für typsicheren TypeScript-Code?
Wir reviewen euren TypeScript-Code, bauen wartbare Utility Types mit infer und conditional types, und sorgen dafür, dass API Clients und Formulare durchgängig typsicher bleiben, von der Magento-Integration bis zum Frontend-Build.
TypeScript Code-Review
Konsistente, wartbare Typdefinitionen statt Duplikaten und any
Utility-Type-Refactoring
Eigene infer-basierte Utility Types statt copy-paste Typen
Typsichere API-Clients
Response- und Request-Typen automatisch aus Schemas abgeleitet
10. Zusammenfassung
Das infer Schlüsselwort löst ein sehr konkretes Problem: Typen aus bestehenden Typen ableiten, statt sie manuell zu duplizieren. Es funktioniert ausschließlich innerhalb der extends-Klausel eines conditional type, wo es wie eine Capture Group eine Typvariable an einer bestimmten strukturellen Position deklariert und mit dem passenden Teil des untersuchten Typs füllt. Die eingebauten Utility Types ReturnType, Parameters und Awaited sind im Kern nichts anderes als kompakte, gut getestete Anwendungen genau dieses Mechanismus.
Wer eigene Utility Types mit infer baut, sollte auf drei Dinge achten: die Grenzen des Mechanismus kennen, also kein infer außerhalb eines conditional type verwenden, die Auswirkungen der distributiven Auswertung bei Union-Typen einplanen, und komplexe Konstruktionen in benannte, lesbare Zwischenschritte aufteilen. Richtig eingesetzt sorgt infer dafür, dass abgeleitete Typen automatisch mit ihrer Quelle synchron bleiben, ganz ohne zusätzlichen Wartungsaufwand bei jeder Änderung.
Das infer-Schlüsselwort - Das Wichtigste auf einen Blick
Mechanik
infer deklariert eine Typvariable ausschließlich innerhalb der extends-Klausel eines conditional type und erfasst die passende Teilstruktur.
Promise & Awaited
Der aufgelöste Wert eines Promise lässt sich mit infer extrahieren, verschachtelte Promises benötigen eine rekursive Variante wie Awaited.
Funktionen
Parameter und Rückgabetyp einer Funktion lassen sich einzeln oder gemeinsam über mehrere infer-Positionen erfassen.
Grenzen
infer funktioniert nicht standalone, verteilt sich bei Unions automatisch und sollte in komplexen Ketten in Zwischenschritte aufgeteilt werden.