Les génériques TypeScript expliqués simplement (avec exemples)
Le <T> entre chevrons est probablement le morceau de syntaxe TypeScript qui fait le plus reculer les débutants. Il a l'air abstrait, il s'affiche partout dans les bibliothèques, et la plupart des explications commencent par des mots qui font peur. Pourtant, les génériques répondent à une question très concrète : comment écrire une fonction qui marche pour plusieurs types de données sans perdre le type au passage ?
Cet article part de zéro : le problème d'abord, la syntaxe ensuite, puis les trois outils qu'on croise vraiment en pratique (l'inférence, extends et keyof). Si vous savez écrire une fonction JavaScript, par exemple si vous avez suivi le projet Snake en JavaScript, vous avez le niveau qu'il faut : TypeScript n'est que du JavaScript avec des annotations de types.
Une précision de méthode : tous les extraits de cette page ont été compilés avec TypeScript 5.9.3 en mode strict et exécutés avec Node.js 24 pendant la rédaction. Les messages d'erreur sont cités tels que le compilateur les affiche (en anglais : tsc n'est pas traduit), avec leur code. Vous pouvez rejouer chaque essai chez vous.
Le problème : une fonction qui perd les types
Imaginons une fonction toute simple qui renvoie le premier élément d'un tableau. En JavaScript, aucune question à se poser. En TypeScript, il faut annoncer le type du paramètre, et c'est là que ça se corse : premier élément d'un tableau de quoi ? De nombres ? De textes ? La tentation est grande d'écrire any, le type qui accepte tout :
function premier(tableau: any[]) {
return tableau[0];
}
const nombre = premier([10, 20, 30]);
console.log(nombre.toUpperCase());Ce code compile sans aucune remarque. Relisez pourtant la dernière ligne : j'appelle toUpperCase(), une méthode de chaîne de caractères, sur ce qui est en réalité le nombre 10. À l'exécution, Node s'arrête net :
TypeError: nombre.toUpperCase is not a functionC'est exactement le genre de bug que TypeScript est censé attraper avant l'exécution. Mais any le lui interdit : ce type signifie « ne vérifie rien sur cette valeur ». En entrant dans la fonction, mon tableau de nombres a perdu son identité ; en sortant, la valeur peut prétendre être n'importe quoi. Le typage a un trou au milieu.
La solution : un type qui traverse la fonction
Ce qu'on voudrait dire au compilateur, c'est : « cette fonction accepte un tableau de n'importe quel type, et renvoie un élément du même type ». Les génériques servent précisément à exprimer ce « même type ». On donne un nom à ce type inconnu, par convention T, et on le déclare entre chevrons juste après le nom de la fonction :
function premier<T>(tableau: T[]): T {
return tableau[0];
}
const nombre = premier([10, 20, 30]);
console.log(nombre.toUpperCase());La signature se lit ainsi : <T> déclare un paramètre de type nommé T, tableau: T[] dit que l'argument est un tableau de T, et le : T final promet que la valeur renvoyée est un T. Le type n'est plus perdu au milieu : il entre, il traverse, il ressort. Et cette fois, le compilateur voit le bug :
error TS2339: Property 'toUpperCase' does not exist on type 'number'.Même code appelant, même bug, mais il est signalé à la compilation au lieu d'exploser à l'exécution. C'est toute la promesse des génériques : garder la souplesse d'une fonction qui accepte tous les types, sans renoncer à la vérification.
any, le type se perd dans la fonction. Avec un générique, il la traverse.L'inférence : vous n'écrivez presque jamais les chevrons
Bonne nouvelle : dans l'immense majorité des appels, vous n'avez pas à préciser T. Le compilateur le déduit de l'argument, c'est ce qu'on appelle l'inférence. J'ai vérifié sur trois appels, en affichant à chaque fois le type réel de la valeur à l'exécution :
const a = premier([10, 20, 30]); // T déduit : number
const b = premier(["html", "css"]); // T déduit : string
const d = premier([1, "deux", 3]); // T déduit : string | number
console.log(typeof a, a); // number 10
console.log(typeof b, b); // string html
console.log(typeof d, d); // number 1
console.log(b.toUpperCase()); // HTML : le compilateur sait que b est stringLes commentaires sont les sorties réelles de mon terminal. Deux choses à retenir. D'abord, b.toUpperCase() compile sans erreur : le compilateur sait que b est une chaîne, la preuve que le type a bien traversé. Ensuite, le tableau mélangé de la troisième ligne ne provoque pas d'erreur : TypeScript déduit le type le plus précis qui couvre tous les éléments, ici l'union string | number. On peut aussi forcer T à la main avec premier<string>(...), mais on ne le fait en pratique que quand le compilateur n'a rien pour deviner.
Le piège à connaître : le générique ne vérifie que ce que vous déclarez
Justement, voici le cas où forcer T à la main devient un mensonge. Que renvoie premier<string>([]), le premier élément d'un tableau vide ? Ma signature promet un T, donc une chaîne. J'ai mesuré :
const c = premier<string>([]);
console.log(typeof c, c); // undefined undefinedLa valeur est undefined, alors que le compilateur, lui, croit dur comme fer que c est une chaîne : il laisse passer c.toUpperCase() sans un mot, et le programme plante à l'exécution. Ce n'est pas un bug de TypeScript : par défaut, il considère qu'un accès par index comme tableau[0] renvoie bien un élément. C'est ma signature qui promet plus que ce que le code garantit.
La version honnête déclare que le résultat peut être undefined, et le compilateur change immédiatement de comportement :
function premier<T>(tableau: T[]): T | undefined {
return tableau[0];
}
const langage = premier(["html", "css"]);
console.log(langage.toUpperCase());
// error TS18048: 'langage' is possibly 'undefined'.Il force maintenant à traiter le cas du tableau vide (par exemple avec if (langage !== undefined)) avant d'utiliser la valeur. Mon avis tranché : prenez cette version. Un générique n'est pas un talisman, il vérifie exactement ce que vous déclarez, ni plus ni moins ; autant déclarer la vérité.
extends : poser une condition d'entrée sur T
Par défaut, T accepte absolument tout. Mais dès que la fonction utilise une propriété de la valeur, il faut le dire. Exemple : une fonction qui compare deux valeurs par leur longueur. Elle a besoin que T possède une propriété length, et c'est la contrainte extends qui l'exprime :
function plusLong<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(plusLong("pomme", "kiwi")); // pomme
console.log(plusLong([1, 2, 3], [4, 5])); // [ 1, 2, 3 ]T extends { length: number } se lit « n'importe quel type, à condition qu'il ait une propriété length de type number ». Les chaînes et les tableaux remplissent le contrat, la fonction marche pour les deux sans que j'aie écrit deux versions. Un nombre, en revanche, n'a pas de longueur, et le compilateur refuse l'appel à l'entrée :
plusLong(10, 20);
// error TS2345: Argument of type 'number' is not assignable
// to parameter of type '{ length: number; }'.extends filtre les types à l'entrée : pas de length, pas d'appel.keyof : limiter T aux clés d'un objet
La contrainte la plus utile au quotidien combine extends avec l'opérateur keyof, qui désigne l'ensemble des clés d'un type. Elle permet d'écrire une fonction qui lit une propriété d'un objet en interdisant les clés qui n'existent pas :
function propriete<T, K extends keyof T>(objet: T, cle: K): T[K] {
return objet[cle];
}
const profil = { pseudo: "sam", articles: 19, actif: true };
console.log(propriete(profil, "pseudo")); // sam
console.log(propriete(profil, "articles")); // 19Deux paramètres de type cette fois, séparés par une virgule : T est le type de l'objet, K est contraint aux clés de T, et le retour T[K] est le type exact de la propriété demandée (string pour pseudo, number pour articles). Le compilateur connaît donc la liste des clés valides, et il la récite dans le message d'erreur quand on se trompe :
propriete(profil, "email");
// error TS2345: Argument of type '"email"' is not assignable
// to parameter of type '"pseudo" | "articles" | "actif"'.Regardez bien ce message : le type attendu est littéralement l'union des trois clés de mon objet. Une faute de frappe dans un nom de propriété, l'erreur classique de JavaScript qui produit des undefined silencieux, devient une erreur de compilation avec la liste des valeurs possibles sous les yeux.
Des types génériques, pas seulement des fonctions
Les chevrons ne se limitent pas aux fonctions : un alias de type peut aussi prendre un paramètre. Le cas d'école est la réponse d'une API, dont l'enveloppe est toujours la même alors que le contenu change d'un appel à l'autre :
type ReponseApi<T> = {
ok: boolean;
donnees: T;
};
type Article = { titre: string; slug: string };
type Profil = { pseudo: string; articles: number };
const rep1: ReponseApi<Article[]> = {
ok: true,
donnees: [{ titre: "Flexbox expliqué simplement", slug: "flexbox" }],
};
const rep2: ReponseApi<Profil> = {
ok: true,
donnees: { pseudo: "sam", articles: 19 },
};Un seul type d'enveloppe, deux contenus différents. Et le compilateur suit le contenu à la trace : rep1.donnees[0].slug compile et affiche bien flexbox, tandis qu'un accès par index sur rep2, dont les données ne sont pas un tableau, est refusé avec l'erreur TS7053: Element implicitly has an 'any' type [...] Property '0' does not exist on type 'Profil'. Vous utilisez d'ailleurs des types génériques depuis votre premier fichier TypeScript sans le savoir : string[] n'est qu'un raccourci pour Array<string>, et une fonction asynchrone renvoie une Promise<T>. Les chevrons des bibliothèques que vous croisez sont exactement le mécanisme de cet article.
Quand ne pas écrire de générique
Un dernier conseil, parce que la tentation inverse existe aussi. Un générique ne se justifie que si le paramètre de type relie au moins deux choses entre elles : deux arguments, ou un argument et le retour. Une signature comme function affiche<T>(valeur: T): void, où T n'apparaît qu'une fois, n'apporte rien : valeur: unknown dit la même chose plus simplement. Si vous ne savez pas quel nom donner à la relation que votre T exprime, c'est souvent qu'il n'y en a pas.
Vous avez maintenant les quatre outils qui couvrent l'essentiel des besoins réels : l'inférence qui écrit les types à votre place, une signature honnête qui déclare ce qui peut mal se passer, extends pour poser des conditions et keyof pour verrouiller les clés. La suite logique de ce blog sera de les mettre en pratique dans un vrai projet TypeScript de bout en bout. Et si vous vous demandez encore si tout cet outillage vaut l'effort d'apprentissage, les raisons de devenir développeur n'ont pas changé : TypeScript est simplement devenu l'un des tickets d'entrée les plus demandés du métier.