Back

`infer` in TypeScript verstehen

`infer` in TypeScript verstehen

infer deklariert eine Typvariable innerhalb der extends-Klausel eines bedingten Typs und erfasst dabei einen übereinstimmenden Teiltyp, sodass dieser im True-Zweig verwendet werden kann — außerhalb dieser Position hat es keine Bedeutung und führt an jeder anderen Stelle zu einem Kompilierfehler. Wenn Sie schon einmal die .d.ts-Datei einer Bibliothek oder den PR eines Kollegen geöffnet und etwas wie T extends (...args: any[]) => infer R ? R : any gesehen haben, verwendet diese Zeile infer, um ein Stück aus einem Typ herauszulösen — ähnlich wie Destructuring einen Wert aus einem Objekt extrahiert.

Wesentliche Erkenntnisse

  • infer ist ausschließlich innerhalb der extends-Klausel eines bedingten Typs zulässig, und die damit eingeführte Variable ist nur im True-Zweig gültig — eine Referenzierung im False-Zweig ist ein Compilerfehler.
  • infer wurde in TypeScript 2.8 eingeführt; Template-Literal-infer kam in 4.1, Awaited<T> in 4.5 und infer X extends Constraint in 4.7.
  • Wenn der Typparameter eine nackte Typvariable ist und Sie einen Union-Typ übergeben, verteilt sich der bedingte Typ über jedes Member, sodass infer einmal pro Member aufgelöst wird und die Ergebnisse zu einem Union-Typ zusammengefasst werden — ein häufig übersehener Fehler, der zu unbeabsichtigter Typverbreiterung führt.
  • Seit TypeScript 4.5 ist Awaited<T> der eingebaute Typ zum Auflösen von Promise-Ketten; es gibt keinen Grund, UnwrapPromise in modernem TypeScript manuell zu implementieren.
  • Mehrere Kandidaten für dieselbe infer-Variable erzeugen in kovarianten Positionen einen Union-Typ und in kontravarianten Positionen einen Intersection-Typ.

Bedingte Typen: das Fundament, auf dem infer aufbaut

Ein bedingter Typ wählt einen von zwei Typen aus, basierend darauf, ob ein Typ einem anderen zuweisbar ist. Die Syntax lautet T extends X ? A : B. Er funktioniert wie ein ternärer Operator, jedoch auf Typebene: Wenn T X zuweisbar ist, wird der Typ zu A aufgelöst, andernfalls zu B. Das TypeScript-Handbuch zu bedingten Typen ist die primäre Referenz.

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;      // false

Das Verhalten, das häufig zu Verwirrung führt, ist die Distributivität. Wenn der geprüfte Typ ein nackter Typparameter ist — also T direkt, ohne Einbettung — und Sie einen Union-Typ übergeben, verteilt TypeScript den bedingten Typ über jedes Union-Member separat und fasst die Ergebnisse zu einem neuen Union-Typ zusammen. Das Einbetten des Parameters in ein Tupel ([T] extends [X]) deaktiviert die Distribution und prüft den Union-Typ als eine Einheit. Dieser Unterschied ist unter distributive bedingte Typen dokumentiert.

type Distributed<T> = T extends string ? T[] : never;
type R1 = Distributed<string | number>; // string[] (das number-Member wird zu never aufgelöst und eliminiert)

type NonDistributed<T> = [T] extends [string] ? T[] : never;
type R2 = NonDistributed<string | number>; // never — wird als Einheit geprüft

Behalten Sie diese Regel im Hinterkopf: Sie ist die Ursache der häufigsten infer-Überraschungen, auf die im Abschnitt zu den Fallstricken eingegangen wird.

Anatomie eines infer-Musters

Eine mit infer eingeführte Typvariable ist nur im True-Zweig des bedingten Typs gültig; eine Referenzierung im False-Zweig ist ein TypeScript-Compilerfehler. infer führt die Variable innerhalb eines extends-Musters ein und bindet sie an das, was bei der Auswertung des bedingten Typs an dieser Position übereinstimmt. Dadurch verhält sich infer ähnlich wie Destructuring — aber für Typen: Sie schreiben ein Muster, das die erwartete Form widerspiegelt, benennen die zu extrahierenden Teile, und TypeScript füllt sie aus.

type ElementType<T> = T extends (infer U)[] ? U : T;

type A = ElementType<string[]>; // string
type B = ElementType<number>;   // number

Das Muster (infer U)[] bedeutet: „ein Array eines bestimmten Elementtyps — nenne diesen Elementtyp U.” Wenn T gleich string[] ist, wird U an string gebunden. Schlägt die Übereinstimmung fehl, wird der False-Zweig ausgeführt, und U ist dort nicht mehr verfügbar:

type Bad<T> = T extends (infer U)[] ? U[] : U;
//                                          ^ Fehler: Der Name 'U' wurde nicht gefunden.

Ein einzelnes Muster kann mehrere infer-Variablen enthalten, von denen jede an ihre eigene Position gebunden wird:

type SplitFn<T> = T extends (arg: infer A) => infer R ? [A, R] : never;

type S = SplitFn<(x: number) => string>; // [number, string]

Hier erfasst infer A den Parametertyp und infer R den Rückgabetyp in einem einzigen Durchgang. Genau diese Technik verwenden die eingebauten Utility-Typen.

infer in der Standardbibliothek: die kanonischen Extraktoren

Die TypeScript-Standardbibliothek implementiert ihre Extraktor-Utility-Typen mit infer. Jeder folgt derselben Form: ein Muster abgleichen, die interessante Position binden und im True-Zweig zurückgeben. Die nachstehenden Definitionen stammen aus lib/es5.d.ts.

Rückgabetyp einer Funktion — ReturnType

ReturnType<T> extrahiert den Rückgabetyp eines Funktionstyps, indem es die Funktionsform abgleicht und die Rückgabeposition an infer R bindet. Die Definition in der Standardbibliothek schränkt T auf aufrufbare Typen ein.

type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

type A = ReturnType<() => string>;        // string
type B = ReturnType<() => { id: number }>; // { id: number }

Beachten Sie die generische Einschränkung T extends (...args: any) => any — sie ist Teil der tatsächlichen Standardbibliotheksdefinition und kein optionaler Zusatz.

Fazit: ReturnType ist das klarste Anwendungsbeispiel für infer — die Funktion per Muster abgleichen und den Rückgabeslot erfassen.

Funktionsparameter als Tupel — Parameters

Parameters<T> extrahiert die Parameterliste einer Funktion als Tupel, indem die Rest-Parameter-Position an infer P gebunden wird.

type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

type Args = Parameters<(a: string, b: number) => void>; // [a: string, b: number]

Fazit: Da Parameter als Tupeltyp modelliert werden, liefert infer P über ...args die gesamte Liste, einschließlich Bezeichnungen und Optionalität.

Promise-Auflösung — Awaited<T> verwenden, kein manueller Unwrapper

Seit TypeScript 4.5 entfaltet das eingebaute Awaited<T> rekursiv Promise-Ketten und behandelt PromiseLike — in modernem TypeScript gibt es keinen Grund, UnwrapPromise manuell zu implementieren. Viele ältere Tutorials definieren ihr eigenes T extends Promise<infer U> ? U : T, aber diese einstufige Variante entfaltet keine verschachtelten Promises und modelliert keine .then-fähigen Objekte. Awaited leistet beides, wie in den TypeScript 4.5 Release Notes beschrieben.

type A = Awaited<Promise<string>>;            // string
type B = Awaited<Promise<Promise<number>>>;   // number — rekursiv
type C = Awaited<boolean | Promise<number>>;  // number | boolean

Fazit: Verwenden Sie direkt Awaited<T>. Schreiben Sie einen eigenen infer-basierten Promise-Unwrapper nur als Lernübung.

Array-Elementtyp

Den Elementtyp eines Arrays zu extrahieren ist das kleinste nützliche infer-Muster: (infer U)[] abgleichen und U zurückgeben.

type ElementOf<T> = T extends readonly (infer U)[] ? U : never;

type A = ElementOf<number[]>;          // number
type B = ElementOf<readonly string[]>; // string

Fazit: Nehmen Sie readonly in das Muster auf, damit der Utility-Typ sowohl für veränderliche als auch für schreibgeschützte Arrays funktioniert.

Tupel-Head, -Tail und -Rest

Tupel unterstützen positionelle infer-Muster mit Rest-Elementen, mit denen Sie den ersten, die verbleibenden oder den letzten Wert extrahieren können. Die Muster spiegeln JavaScript-Array-Destructuring wider.

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type H = Head<[1, 2, 3]>; // 1
type R = Tail<[1, 2, 3]>; // [2, 3]
type L = Last<[1, 2, 3]>; // 3

Fazit: [infer Head, ...infer Rest] und [...any[], infer Last] sind die Bausteine für rekursive Tupelmanipulation — dieselben Muster bilden die Grundlage für typsichere Router und Formzustandsbibliotheken auf Typebene.

Rekursive Inferenz — Flatten

infer in Kombination mit Rekursion ermöglicht es einem Typ, beliebig tief verschachtelte Strukturen aufzufalten. Flatten durchläuft rekursiv verschachtelte Arrays, bis ein Nicht-Array-Typ erreicht wird.

type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;

type A = Flatten<number[][][]>; // number
type B = Flatten<string>;       // string

In jedem Durchgang wird U an den Elementtyp gebunden und wieder in Flatten eingespeist. Wenn T kein Array mehr ist, gibt der False-Zweig ihn unverändert zurück.

Fazit: Rekursives infer ist der Weg, um verschachtelte Container aufzufalten; dieselbe Form entfaltet tief verschachtelte Promise- oder Objekttypen.

Template-Literal-Inferenz — Strings auf Typebene parsen

Template-Literal-Typen unterstützen infer innerhalb ihrer Substitutionspositionen. Das nachstehende ExtractId<T>-Beispiel gleicht das String-Muster ab und bindet das variable Segment an Id, wodurch typsichere Routenparameter-Extraktion ohne einen Laufzeit-Parser möglich wird — ein Muster, das seit TypeScript 4.1 verfügbar ist.

type ExtractId<T> = T extends `/users/${infer Id}` ? Id : never;

type A = ExtractId<"/users/42">;   // "42"
type B = ExtractId<"/posts/42">;   // never

Sie können infer über mehrere Segmente hinweg verketten, um einen vollständigen Pfad in seine Bestandteile zu zerlegen:

type RouteParams<T> =
  T extends `${infer _Start}/:${infer Param}/${infer Rest}`
    ? Param | RouteParams<`/${Rest}`>
    : T extends `${infer _Start}/:${infer Param}`
      ? Param
      : never;

type P = RouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"

Dies ist das typebene Äquivalent eines Pfad-Parsers und bildet die Grundlage für typsicheres Routing in Frameworks und Bibliotheken, die Parameterobjekte aus Routen-Strings ableiten.

Fazit: Template-Literal-infer wandelt String-förmige Typen ohne Laufzeitkosten in strukturierte Daten um — nützlich für Routenparameter, CSS-Property-Unions und das Parsen von markierten String-Formaten.

Eingeschränkte Inferenz: infer X extends Constraint (TS 4.7)

TypeScript 4.7 hat infer X extends Constraint eingeführt, wodurch ein bedingter Typ eine inferierte Variable direkt beim Abgleich einschränken kann — Inferenz und Einschränkungsprüfung werden in einem einzigen Schritt kombiniert. Dies ist in den TypeScript 4.7 Release Notes dokumentiert. Vor 4.7 musste man zunächst inferieren und dann in einem zweiten verschachtelten bedingten Typ das Ergebnis einschränken.

// Vor 4.7: inferieren, dann in einem zweiten bedingten Typ prüfen
type FirstStringOld<T> =
  T extends [infer H, ...any[]]
    ? H extends string ? H : never
    : never;

// Ab 4.7: Einschränkung direkt beim Inferieren
type FirstString<T> =
  T extends [infer H extends string, ...any[]] ? H : never;

type A = FirstString<["hi", 1, 2]>; // "hi"
type B = FirstString<[1, 2, 3]>;    // never

TypeScript 4.8 hat die eingeschränkte Inferenz für primitive Typen wie number, bigint und boolean später weiter verbessert, sodass der Compiler beim Abgleich von Template-String-Mustern präzisere Literaltypen beibehalten kann. Dies baut auf der in TypeScript 4.7 eingeführten eingeschränkten infer-Syntax auf, wurde jedoch als separate Verbesserung ausgeliefert.

Fazit: Verwenden Sie infer X extends C, wenn Sie sowohl den erfassten Typ als auch eine Garantie über seine Form in einem einzigen Ausdruck benötigen — es ersetzt das ältere Muster aus Inferenz und anschließender Verschachtelung.

Drei Fallstricke, die die Inferenz still brechen

Die drei Fallstricke, die infer still brechen, sind: Distributivität über Union-Typen, die Ergebnisse verbreitert; Varianzumkehrung, die mehrere infer-Kandidaten zwischen Union und Intersection wechseln lässt; und zu spezifische Muster, die in den False-Zweig fallen. infer-Fehler sind in der Regel lautlos — der Typ kompiliert weiterhin, ist aber breiter oder enger als beabsichtigt.

1. Distributivität verbreitert Ergebnisse unbemerkt

Wenn der Typparameter T eine nackte Typvariable ist und Sie einen Union-Typ übergeben, verteilt TypeScript den bedingten Typ über jedes Member separat — das bedeutet, infer R wird einmal pro Union-Member aufgelöst und die Ergebnisse werden zu einem Union-Typ zusammengefasst, was den inferierten Typ unbeabsichtigt verbreitern kann.

type Unwrap<T> = T extends Promise<infer U> ? U : T;

// Sieht aus, als würde ein Typ zurückgegeben; tatsächlich wird distribuiert
type R = Unwrap<Promise<string> | Promise<number>>; // string | number

Wenn Sie gemischte Unions ablehnen statt zusammenfassen möchten, betten Sie den Parameter ein, um die Distribution zu deaktivieren:

type UnwrapStrict<T> = [T] extends [Promise<infer U>] ? U : T;

Fazit: Ein nacktes T plus ein Union-Typ bedeutet Auswertung pro Member. Betten Sie in [T] ein, wenn der Union-Typ als Einheit behandelt werden soll.

2. Kovariante vs. kontravariante Position verändert das Ergebnis

Mehrere Kandidaten für dieselbe infer-Variable erzeugen in kovarianten Positionen (z. B. Rückgabetypen) einen Union-Typ und in kontravarianten Positionen (z. B. Funktionsparametertypen) einen Intersection-Typ. Bei überladenen Funktionen verwendet die Inferenz die letzte Signatur. Dieses Verhalten ist in den TypeScript 2.8 Release Notes beschrieben.

// Kovariant (Rückgabeposition): Union
type Co<T> = T extends { a: () => infer U; b: () => infer U } ? U : never;
type C = Co<{ a: () => string; b: () => number }>; // string | number

// Kontravariant (Parameterposition): Intersection
type Contra<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type D = Contra<{ a: (x: string) => void; b: (x: number) => void }>; // string & number

Fazit: Denselben infer-Namen über mehrere Positionen hinweg wiederzuverwenden ist bewusst und leistungsstark, aber ob Sie einen Union- oder Intersection-Typ erhalten, hängt von der Varianz der Positionen ab. Gehen Sie nicht automatisch von einem Union-Typ aus.

3. Nicht übereinstimmende Muster fallen in den False-Zweig

infer bindet nur, wenn der Kandidatentyp strukturell mit dem Muster übereinstimmt. Stimmt die Form nicht überein, nimmt der bedingte Typ den False-Zweig, und die inferierte Variable wird nie aufgelöst — ein zu spezifisches Muster gibt also lautlos Ihren Fallback-Typ zurück, statt des erwarteten Werts.

type FirstArg<T> = T extends (a: infer A, b: infer B) => any ? A : never;

type X = FirstArg<string>; // never — string stimmt nicht mit einem Funktionstyp überein

In diesem Beispiel erwartet FirstArg einen Funktionstyp. Die Übergabe von string stimmt nicht mit diesem Muster überein, sodass der bedingte Typ den False-Zweig auflöst und never zurückgibt.

Fazit: Halten Sie Muster so offen wie der Abgleich erlaubt. Verwenden Sie ...args: infer P oder [infer H, ...any[]] statt Formen mit fester Stelligkeit, es sei denn, Sie möchten andere Stelligkeiten gezielt ablehnen.

Wo infer in der Praxis begegnet

Im Anwendungscode schreiben Sie infer selten selbst — Sie begegnen ihm in Bibliotheks-Typdefinitionen wie Reacts ComponentProps, Express’ RequestHandler, Redux Toolkits PayloadAction und RTK Query-Endpunkttypen und verwenden die daraus resultierenden Utility-Typen. Das Muster T extends Wrapper<infer X> ? X : Fallback zu erkennen, ermöglicht es Ihnen, diese Definitionen zu lesen, statt sie als Black Box zu behandeln.

React ComponentProps. Die @types/react-Typdefinitionen definieren ComponentProps mit infer, um die Props einer Komponente aus ihrem Typ zu extrahieren, und verzweigen dabei zwischen Host-Elementen und Komponentenkonstruktoren. Beim Lesen erkennen Sie dieselbe T extends ... infer P ... ? P : ...-Form wie bei ReturnType, angewendet auf Reacts Elementtypen.

Express Request-Handler. Express’ @types/express modelliert RequestHandler als Generic mit Parametern für Routenparameter, Response-Body, Request-Body und Query. Bibliotheken, die typisierte Routen-Handler aus einem Pfad-String ableiten, kombinieren diese Generics mit Template-Literal-infer, um :param-Segmente zu extrahieren — das zuvor gezeigte RouteParams-Muster ist der Kern dieser Mechanik.

Redux Toolkit PayloadAction. PayloadAction aus Redux Toolkit ist ein nützlicher Typ, aus dem man mit infer extrahieren kann, obwohl seine eigene Definition eher auf Generics und bedingte Typen als auf infer intern setzt — eine Erinnerung daran, dass infer die Art ist, wie Sie die Typen einer Bibliothek lesen, nicht unbedingt wie die Bibliothek selbst geschrieben ist. Häufig schreibt man type Payload<A> = A extends PayloadAction<infer P> ? P : never, um den Payload-Typ eines Reducers aus seiner Action zu ermitteln.

import type { PayloadAction } from "@reduxjs/toolkit";

type PayloadOf<A> = A extends PayloadAction<infer P> ? P : never;

type P = PayloadOf<PayloadAction<{ id: string }>>; // { id: string }

RTK Query-Endpunkte. RTK Query (Teil von Redux Toolkit) stellt generierte Endpunkttypen bereit, aus denen Query- und Ergebnistypen mit infer-basierten Utility-Typen wiederhergestellt werden können. Den Ergebnistyp eines Endpunkts mit einem bedingten Typ zu extrahieren, der die Ergebnisposition bindet, ist eine Routineaufgabe, sobald man das Muster erkannt hat.

Zusammenfassung

infer ist ein Feature mit einer einzigen Regel: Es benennt eine Position innerhalb eines bedingten Typmusters und bindet das, was dort übereinstimmt — verwendbar ausschließlich im True-Zweig. Sobald Sie T extends Pattern<infer X> ? X : Fallback als Destructuring auf Typebene verstehen, liest sich jeder eingebaute Utility-Typ und jede Bibliotheks-Typdefinition als Variation dieser einen Grundbewegung. Wenn Sie das nächste Mal infer in einer .d.ts-Datei sehen, verfolgen Sie das Muster: Finden Sie die Position, an der die Variable sitzt, prüfen Sie, ob der Parameter nackt oder eingebettet ist, und vergewissern Sie sich, dass Sie den True-Zweig lesen. Beginnen Sie damit, jedes manuell implementierte UnwrapPromise in Ihrer Codebasis durch Awaited<T> zu ersetzen, und verwenden Sie infer X extends Constraint, wo Sie derzeit einen zweiten bedingten Typ verschachteln, um ein Ergebnis einzuschränken.

FAQs

infer extrahiert einen Typ aus einem bestehenden Typ durch Musterabgleich innerhalb eines bedingten Typs, während ein Mapped Type jede Eigenschaft eines Typs in eine neue Form transformiert. Verwenden Sie infer, wenn Sie einen Teiltyp aus einer Struktur lesen müssen, etwa den Rückgabetyp einer Funktion oder den Elementtyp eines Arrays. Verwenden Sie einen Mapped Type, wenn Sie über Schlüssel iterieren und einen neuen Objekttyp erzeugen möchten. Sie lösen entgegengesetzte Probleme: infer liest, Mapped Types schreiben um.

Nein. Eine mit infer eingeführte Typvariable ist nur im True-Zweig des bedingten Typs gültig; eine Referenzierung im False-Zweig erzeugt einen TypeScript-Compilerfehler wie 'Der Name wurde nicht gefunden'. Die Variable existiert nur, wenn ein Abgleich erfolgreich war, daher hat der False-Zweig keinen Wert zum Binden. Wenn Sie in beiden Zweigen einen Wert benötigen, inferieren Sie im True-Zweig und geben Sie im False-Zweig einen unabhängigen Fallback-Typ an.

Wegen der Distributivität. Wenn der Typparameter eine nackte Typvariable ist und Sie einen Union-Typ übergeben, verteilt TypeScript den bedingten Typ über jedes Member separat, sodass infer einmal pro Union-Member aufgelöst wird und die Ergebnisse zu einem neuen Union-Typ zusammengefasst werden. Dies kann das Ergebnis unbeabsichtigt verbreitern. Um die Distribution zu deaktivieren und den Union-Typ als Einheit zu prüfen, betten Sie den Parameter in ein Tupel ein, wie in [T] extends [Promise<infer U>] ? U : T.

Nein, nicht in modernem TypeScript. Seit TypeScript 4.5 entfaltet das eingebaute Awaited<T> rekursiv Promise-Ketten und behandelt PromiseLike-Thenable-Objekte. Ein manuell erstelltes T extends Promise<infer U> ? U : T entfaltet nur eine Ebene und ignoriert Thenable-Objekte, sodass es bei verschachtelten Promises versagt. Verwenden Sie im Anwendungscode direkt Awaited<T>, und schreiben Sie einen eigenen infer-basierten Unwrapper nur als Lernübung.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay