Un vincolo generico limita ciò che un parametro di tipo può essere. <T extends { length: number }> accetta solo tipi che hanno un length numerico (stringhe, array, i tuoi oggetti), e grazie a questa promessa la funzione può leggere .length.
Il tipo di ritorno resta T, non { length: number }: passando due array di numeri ottieni number[], e gli oggetti conservano il loro tag. Il vincolo descrive il minimo; il tipo di chi chiama viene preservato.
Perché servono i vincoli
Dentro una funzione generica, un T senza vincoli potrebbe essere qualsiasi cosa: una stringa, un numero, null, una funzione. Quindi TypeScript permette solo ciò che funziona su ogni tipo, cioè quasi nulla.
Il compilatore stampa:
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'.
La soluzione è dire cosa ti serve: <T extends { length: number }>. I vincoli trasformano "qualsiasi tipo" in "qualsiasi tipo che abbia questo", che di solito è ciò che intende una funzione generica.
Vincolare a un'interfaccia
Il vincolo può essere qualsiasi tipo, anche un'interfaccia o un alias di tipo che hai già. La funzione lavora allora con qualunque cosa abbia almeno quei membri, e restituisce comunque il tipo completo di chi chiama.
Confrontalo con un normale tipo di parametro: function byId(items: HasId[], id: number): HasId | undefined accetta gli stessi array, ma il risultato è solo HasId, quindi found.name sarebbe un errore. La versione generica fa passare il tipo completo.
K extends keyof T: accesso sicuro alle proprietà
Il vincolo più comune mette in relazione due parametri di tipo. K extends keyof T dice che K deve essere uno dei nomi di proprietà di T, e T[K] è il tipo di quella proprietà.
Una chiave scritta male o inesistente è un errore di compilazione, e il tipo di ritorno segue la chiave: "year" dà number, "title" dà string. La pagina su keyof spiega keyof in sé, compreso il suo comportamento con le index signature.
Vincolare ai primitivi
Un vincolo può essere un primitivo o un'unione di primitivi. Un effetto collaterale utile: quando T extends string (o number), TypeScript deduce il tipo letterale dell'argomento invece di allargarlo.
Senza il vincolo, createEvent("user.saved") restituisce { type: string }: qui TypeScript allarga a string il letterale dedotto per T. Con T extends string il letterale viene mantenuto, ed è così che gli helper tipizzati per eventi e routing ottengono nomi esatti da semplici argomenti stringa.
Errori comuni
Restituire qualcosa che corrisponde solo al vincolo. Una funzione che restituisce T deve restituire un T, non un valore qualsiasi che rispetti il vincolo:
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'.
Se T è { id: number; name: string }, il nuovo oggetto non ha name, quindi non è un T. Restituisci uno spread dell'input, return { ...item, id: 0 };, che conserva tutte le proprietà, oppure dichiara HasId come tipo di ritorno se è tutto ciò che produci.
Passare un argomento di tipo che viola il vincolo. Anche gli argomenti di tipo espliciti vengono controllati: con function scale<T extends number>(x: T), la chiamata scale<string>("2") fallisce con Type 'string' does not satisfy the constraint 'number'. (TS2344).
Usare un vincolo quando non serve un generic. Se T compare solo nel parametro, function print<T extends HasId>(item: T): void è solo function print(item: HasId): void con sintassi in più. I vincoli contano quando T compare anche nel tipo di ritorno o in un altro parametro.
Vincoli con valori di default
Un parametro di tipo può avere sia un vincolo sia un default. Il default deve rispettare il vincolo.
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'.
La stessa parola chiave extends compare anche nei conditional types, T extends string ? A : B, dove è un test invece che una restrizione; la pagina sui conditional types spiega questa forma.
Domande frequenti
Cosa significa T extends in un generic di TypeScript?
<T extends Constraint> significa che T può essere qualsiasi tipo assegnabile a Constraint. Chi chiama può passare solo tipi del genere, e dentro la funzione puoi usare tutto ciò che il vincolo garantisce. <T extends { length: number }> accetta stringhe, array e qualsiasi oggetto con un length numerico, e ti permette di leggere value.length.
Cosa significa K extends keyof T?
K deve essere uno dei nomi di proprietà di T. Insieme all'indexed access type T[K] tipizza con precisione l'accesso alle proprietà: function get<T, K extends keyof T>(obj: T, key: K): T[K] accetta solo chiavi reali di obj, e il tipo di ritorno è il tipo di quella proprietà.
Perché ottengo "Property does not exist on type T"?
Un T senza vincoli potrebbe essere qualsiasi tipo, anche number o null, quindi TypeScript non permette nulla di specifico su di esso (TS2339). Aggiungi un vincolo che garantisca la proprietà: <T extends { name: string }>, oppure vincolalo a un'interfaccia che hai già, <T extends User>.
Cosa significa "T could be instantiated with a different subtype of constraint"?
Errore TS2322: la funzione promette di restituire T, ma restituisce un valore che corrisponde solo al vincolo. Se T è { id: number; name: string }, un nuovo { id: 1 } non è un T. Restituisci il valore che hai ricevuto (o uno spread di esso, tipizzato di conseguenza), oppure cambia il tipo di ritorno nel vincolo.
Che differenza c'è tra extends in un generic ed extends in una classe o interfaccia?
Stessa parola chiave, idea collegata. In class Dog extends Animal e interface B extends A dichiara un'ereditarietà. In <T extends A> dichiara un vincolo: T deve essere assegnabile ad A. In un conditional type, T extends A ? X : Y, è un test che sceglie uno fra due tipi.