Ein Generic Constraint begrenzt, was ein Typparameter sein kann. <T extends { length: number }> akzeptiert nur Typen mit numerischem length (Strings, Arrays, deine eigenen Objekte), und dank dieses Versprechens darf die Funktion .length lesen.
Der Rückgabetyp ist weiterhin T, nicht { length: number }: Zwei Arrays von Zahlen ergeben wieder number[], und die Objekte behalten ihr tag. Der Constraint beschreibt das Minimum; der Typ des Aufrufers bleibt erhalten.
Warum Constraints nötig sind
In einer generischen Funktion könnte ein T ohne Constraint alles sein: ein String, eine Zahl, null, eine Funktion. Also erlaubt TypeScript nur, was mit jedem Typ funktioniert, und das ist fast nichts.
Der Compiler gibt aus:
index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.
Die Lösung ist, zu sagen, was du brauchst: <T extends { length: number }>. Constraints machen aus „jeder Typ“ „jeder Typ, der das hat“, und das meint eine generische Funktion meistens.
Auf ein Interface einschränken
Der Constraint kann jeder Typ sein, auch ein vorhandenes Interface oder ein Typalias. Die Funktion arbeitet dann mit allem, was mindestens diese Member hat, und gibt trotzdem den vollen Typ des Aufrufers zurück.
Vergleiche mit einem einfachen Parametertyp: function byId(items: HasId[], id: number): HasId | undefined akzeptiert dieselben Arrays, aber das Ergebnis ist nur HasId, also wäre found.name ein Fehler. Die generische Version reicht den vollen Typ durch.
K extends keyof T: sicherer Eigenschaftszugriff
Der häufigste Constraint verbindet zwei Typparameter. K extends keyof T sagt, dass K einer der Eigenschaftsnamen von T sein muss, und T[K] ist der Typ dieser Eigenschaft.
Ein falsch geschriebener oder fehlender Schlüssel ist ein Compilerfehler, und der Rückgabetyp folgt dem Schlüssel: "year" ergibt number, "title" ergibt string. Die Seite zu keyof behandelt keyof selbst, auch sein Verhalten bei Index-Signaturen.
Auf primitive Typen einschränken
Ein Constraint kann ein primitiver Typ oder eine Union davon sein. Ein nützlicher Nebeneffekt: Bei T extends string (oder number) leitet TypeScript den Literaltyp des Arguments ab, statt ihn zu erweitern.
Ohne den Constraint gibt createEvent("user.saved") { type: string } zurück: Hier erweitert TypeScript das für T abgeleitete Literal zu string. Mit T extends string bleibt das Literal erhalten, und so bekommen typisierte Helfer für Events und Routing genaue Namen aus einfachen String-Argumenten.
Häufige Fehler
Etwas zurückgeben, das nur zum Constraint passt. Eine Funktion, die T zurückgibt, muss ein T zurückgeben, nicht irgendeinen Wert, der zum Constraint passt:
interface HasId {
id: number;
}
function reset<T extends HasId>(item: T): T {
return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
// '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
// instantiated with a different subtype of constraint 'HasId'.
Ist T gleich { id: number; name: string }, hat das neue Objekt kein name und ist daher kein T. Gib einen Spread der Eingabe zurück, return { ...item, id: 0 };, der jede Eigenschaft behält, oder deklariere den Rückgabetyp als HasId, wenn das alles ist, was du erzeugst.
Ein Typargument übergeben, das den Constraint verletzt. Auch ausdrückliche Typargumente werden geprüft: Bei function scale<T extends number>(x: T) scheitert der Aufruf scale<string>("2") mit Type 'string' does not satisfy the constraint 'number'. (TS2344).
Einschränken, wenn du gar kein Generic brauchst. Kommt T nur im Parameter vor, ist function print<T extends HasId>(item: T): void nichts anderes als function print(item: HasId): void mit zusätzlicher Syntax. Constraints zählen, wenn T auch im Rückgabetyp oder in einem anderen Parameter vorkommt.
Constraints mit Standardwerten
Ein Typparameter kann sowohl einen Constraint als auch einen Standardwert haben. Der Standardwert muss den Constraint erfüllen.
interface Store<TState extends object = Record<string, unknown>> {
get(): TState;
set(next: Partial<TState>): void;
}
type AnyStore = Store; // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>; // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.
Dasselbe Schlüsselwort extends taucht auch in Conditional Types auf, T extends string ? A : B, wo es ein Test statt einer Einschränkung ist; die Seite zu Conditional Types behandelt diese Form.
Häufig gestellte Fragen
Was bedeutet T extends in einem TypeScript-Generic?
<T extends Constraint> bedeutet, dass T jeder Typ sein kann, der Constraint zuweisbar ist. Aufrufer können nur solche Typen übergeben, und innerhalb der Funktion darfst du alles verwenden, was der Constraint garantiert. <T extends { length: number }> akzeptiert Strings, Arrays und jedes Objekt mit numerischem length und erlaubt dir, value.length zu lesen.
Was bedeutet K extends keyof T?
K muss einer der Eigenschaftsnamen von T sein. Zusammen mit dem Indexed Access Type T[K] typisiert das den Eigenschaftszugriff genau: function get<T, K extends keyof T>(obj: T, key: K): T[K] akzeptiert nur echte Schlüssel von obj, und der Rückgabetyp ist der Typ dieser Eigenschaft.
Warum bekomme ich "Property does not exist on type T"?
Ein T ohne Constraint könnte jeder Typ sein, auch number oder null, daher erlaubt TypeScript nichts Typspezifisches darauf (TS2339). Füge einen Constraint hinzu, der die Eigenschaft verspricht: <T extends { name: string }>, oder schränke auf ein vorhandenes Interface ein, <T extends User>.
Was bedeutet "T could be instantiated with a different subtype of constraint"?
Fehler TS2322: Die Funktion verspricht, T zurückzugeben, gibt aber einen Wert zurück, der nur zum Constraint passt. Ist T gleich { id: number; name: string }, ist ein neues { id: 1 } kein T. Gib den erhaltenen Wert zurück (oder einen entsprechend typisierten Spread davon), oder ändere den Rückgabetyp auf den Constraint.
Was ist der Unterschied zwischen extends in einem Generic und extends in einer Klasse oder einem Interface?
Dasselbe Schlüsselwort, verwandte Idee. In class Dog extends Animal und interface B extends A deklariert es Vererbung. In <T extends A> deklariert es einen Constraint: T muss A zuweisbar sein. In einem Conditional Type, T extends A ? X : Y, ist es ein Test, der einen von zwei Typen wählt.