Une contrainte générique limite ce que peut être un paramètre de type. <T extends { length: number }> n'accepte que les types qui ont une length numérique (chaînes, tableaux, vos propres objets), et grâce à cette promesse, la fonction a le droit de lire .length.
Le type de retour reste T, et non { length: number } : passer deux tableaux de nombres renvoie un number[], et les objets gardent leur tag. La contrainte décrit le minimum ; le type de l'appelant est préservé.
Pourquoi les contraintes sont nécessaires
Dans une fonction générique, un T sans contrainte peut être n'importe quoi : une chaîne, un nombre, null, une fonction. TypeScript n'autorise donc que ce qui fonctionne sur tous les types, c'est-à-dire presque rien.
Le compilateur affiche :
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 correction consiste à dire ce dont vous avez besoin : <T extends { length: number }>. Les contraintes transforment « n'importe quel type » en « n'importe quel type qui a ceci », ce que veut dire en général une fonction générique.
Contraindre à une interface
La contrainte peut être n'importe quel type, y compris une interface ou un alias de type que vous avez déjà. La fonction fonctionne alors avec tout ce qui possède au moins ces membres, et renvoie toujours le type complet de l'appelant.
Comparez avec un simple type de paramètre : function byId(items: HasId[], id: number): HasId | undefined accepte les mêmes tableaux, mais le résultat n'est qu'un HasId, donc found.name serait une erreur. La version générique transmet le type complet.
K extends keyof T : un accès sûr aux propriétés
La contrainte la plus courante relie deux paramètres de type. K extends keyof T indique que K doit être l'un des noms de propriétés de T, et T[K] est le type de cette propriété.
Une clé mal orthographiée ou absente est une erreur de compilation, et le type de retour suit la clé : "year" donne number, "title" donne string. La page keyof présente keyof lui-même, y compris son comportement avec les signatures d'index.
Contraindre à des primitives
Une contrainte peut être une primitive ou une union de primitives. Un effet de bord utile : quand T extends string (ou number), TypeScript infère le type littéral de l'argument au lieu de l'élargir.
Sans la contrainte, createEvent("user.saved") renvoie { type: string } : ici, TypeScript élargit en string le littéral qu'il a inféré pour T. Avec T extends string, le littéral est conservé, et c'est ainsi que les helpers typés d'événements et de routage obtiennent des noms exacts à partir de simples arguments de type chaîne.
Erreurs fréquentes
Renvoyer quelque chose qui ne correspond qu'à la contrainte. Une fonction qui renvoie T doit renvoyer un T, et pas seulement une valeur qui respecte la contrainte :
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'.
Si T est { id: number; name: string }, le nouvel objet n'a pas de name : ce n'est donc pas un T. Renvoyez un étalement de l'entrée, return { ...item, id: 0 };, qui garde toutes les propriétés, ou déclarez le type de retour HasId si c'est tout ce que vous produisez.
Passer un argument de type qui viole la contrainte. Les arguments de type explicites sont vérifiés eux aussi : avec function scale<T extends number>(x: T), l'appel scale<string>("2") échoue avec Type 'string' does not satisfy the constraint 'number'. (TS2344).
Contraindre alors qu'aucun générique n'est nécessaire. Si T n'apparaît que dans le paramètre, function print<T extends HasId>(item: T): void n'est que function print(item: HasId): void avec une syntaxe en plus. Les contraintes comptent quand T apparaît aussi dans le type de retour ou dans un autre paramètre.
Contraintes avec valeurs par défaut
Un paramètre de type peut avoir à la fois une contrainte et une valeur par défaut. La valeur par défaut doit respecter la contrainte.
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'.
Le même mot-clé extends apparaît aussi dans les types conditionnels, T extends string ? A : B, où c'est un test plutôt qu'une restriction ; la page types conditionnels présente cette forme.
Questions fréquentes
Que signifie T extends dans un générique TypeScript ?
<T extends Constraint> signifie que T peut être n'importe quel type assignable à Constraint. Les appelants ne peuvent passer que de tels types, et dans la fonction vous pouvez utiliser tout ce que la contrainte garantit. <T extends { length: number }> accepte les chaînes, les tableaux et tout objet ayant une length numérique, et vous permet de lire value.length.
Que signifie K extends keyof T ?
K doit être l'un des noms de propriétés de T. Combiné au type d'accès indexé T[K], il type exactement l'accès aux propriétés : function get<T, K extends keyof T>(obj: T, key: K): T[K] n'accepte que les vraies clés de obj, et le type de retour est le type de cette propriété.
Pourquoi obtient-on « Property does not exist on type T » ?
Un T sans contrainte peut être n'importe quel type, y compris number ou null : TypeScript n'autorise donc rien de spécifique sur lui (TS2339). Ajoutez une contrainte qui promet la propriété, <T extends { name: string }>, ou contraignez à une interface que vous avez déjà, <T extends User>.
Que signifie « T could be instantiated with a different subtype of constraint » ?
L'erreur TS2322 : la fonction promet de renvoyer T, mais renvoie une valeur qui ne correspond qu'à la contrainte. Si T est { id: number; name: string }, un nouveau { id: 1 } n'est pas un T. Renvoyez la valeur reçue (ou un étalement de celle-ci, typé en conséquence), ou changez le type de retour en la contrainte.
Quelle est la différence entre extends dans un générique et extends dans une classe ou une interface ?
Même mot-clé, idée voisine. Dans class Dog extends Animal et interface B extends A, il déclare un héritage. Dans <T extends A>, il déclare une contrainte : T doit être assignable à A. Dans un type conditionnel, T extends A ? X : Y, c'est un test qui choisit l'un de deux types.