Les generics en TypeScript : le guide
Les generics (ou types génériques) sont l’un des mécanismes les plus puissants de TypeScript, et aussi l’un des plus intimidants au premier abord. Pourtant, l’idée derrière les <T> est simple : écrire du code qui fonctionne avec n’importe quel type, tout en conservant un typage précis. Ce guide décortique les generics pas à pas, de la fonction basique aux types utilitaires de la bibliothèque standard.
Le problème que résolvent les generics
Imaginez une fonction qui renvoie le premier élément d’un tableau. Sans generics, vous avez deux mauvaises options.
// Option 1 : figer le type, donc dupliquer la fonction pour chaque cas
function premierNombre(liste: number[]): number {
return liste[0];
}
// Option 2 : utiliser any, et perdre toute sécurité de typage
function premier(liste: any[]): any {
return liste[0]; // le retour est any : plus aucune autocomplétion
}
Avec any, vous perdez tout ce qui fait l’intérêt de TypeScript. Avec un type figé, vous devez réécrire la fonction pour chaque type de données. Les generics résolvent exactement ce dilemme : ils laissent l’appelant décider du type, sans le figer dans la définition.
Si les fondamentaux du langage ne sont pas encore solides, notre guide TypeScript pose les bases avant d’aborder les génériques.
La syntaxe <T>
Un type générique se déclare entre chevrons, juste après le nom de la fonction. La convention veut qu’on l’appelle T (pour Type), mais ce n’est qu’un nom : T, Elem ou Item fonctionnent aussi bien.
function premier<T>(liste: T[]): T | undefined {
return liste[0];
}
const n = premier([1, 2, 3]); // T inféré : number → n est number | undefined
const s = premier(["a", "b", "c"]); // T inféré : string → s est string | undefined
const b = premier<boolean>([true]); // T explicite : boolean
Le point clé : TypeScript infère le type tout seul dans la plupart des cas. Vous écrivez premier([1, 2, 3]), et il comprend que T vaut number. Vous n’avez besoin de préciser <boolean> que lorsque l’inférence est impossible ou ambiguë. Le résultat garde son type exact, avec l’autocomplétion complète derrière.
Plusieurs paramètres de type
Rien n’oblige à se limiter à un seul type variable. Une fonction qui combine deux valeurs peut en déclarer autant que nécessaire.
function paire<K, V>(cle: K, valeur: V): [K, V] {
return [cle, valeur];
}
const p = paire("age", 30); // [string, number]
// Exemple concret : transformer un tableau en objet indexé
function indexer<T>(liste: T[], cle: (item: T) => string): Record<string, T> {
const resultat: Record<string, T> = {};
for (const item of liste) {
resultat[cle(item)] = item;
}
return resultat;
}
const users = [{ id: "a", nom: "Léa" }, { id: "b", nom: "Tom" }];
const parId = indexer(users, (u) => u.id);
// parId.a est correctement typé { id: string; nom: string }
Les contraintes avec extends
Un générique « nu » comme T peut être absolument n’importe quoi, ce qui limite ce que vous pouvez en faire : impossible d’accéder à .length ou à une propriété précise, puisque TypeScript ne sait rien de T. Le mot-clé extends impose une contrainte : T doit au minimum respecter une certaine forme.
// T doit posséder une propriété length de type number
function plusLong<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
plusLong("bonjour", "salut"); // ✅ les chaînes ont une length
plusLong([1, 2, 3], [4, 5]); // ✅ les tableaux aussi
plusLong(10, 20); // ❌ number n'a pas de length
Un cas très courant : contraindre une clé à appartenir aux propriétés d’un objet, grâce à keyof. C’est le patron exact derrière une fonction de type « lodash get », entièrement typée.
function getProp<T, K extends keyof T>(objet: T, cle: K): T[K] {
return objet[cle];
}
const article = { titre: "Guide", vues: 1200, publie: true };
const t = getProp(article, "titre"); // t est string
const v = getProp(article, "vues"); // v est number
getProp(article, "auteur"); // ❌ "auteur" n'est pas une clé valide
Ici, K extends keyof T garantit que la clé demandée existe vraiment, et T[K] renvoie le type exact de la propriété. Aucune erreur de nom de clé ne peut passer.
Valeurs par défaut
Comme pour les paramètres classiques, un type générique peut recevoir une valeur par défaut avec =. Utile pour proposer un comportement courant sans forcer l’appelant à tout préciser.
interface Reponse<T = unknown> {
succes: boolean;
donnees: T;
}
// Sans préciser T, donnees est de type unknown
const r1: Reponse = { succes: true, donnees: "peu importe" };
// Avec un type explicite, donnees est précisément typé
const r2: Reponse<string[]> = { succes: true, donnees: ["a", "b"] };
Classes et interfaces génériques
Les generics ne se limitent pas aux fonctions. Une classe générique est idéale pour modéliser une structure de données réutilisable, comme une pile (LIFO).
class Pile<T> {
private elements: T[] = [];
empiler(item: T): void {
this.elements.push(item);
}
depiler(): T | undefined {
return this.elements.pop();
}
get taille(): number {
return this.elements.length;
}
}
const nombres = new Pile<number>();
nombres.empiler(1);
nombres.empiler(2);
const dernier = nombres.depiler(); // typé number | undefined
Une interface générique décrit de la même façon un contrat paramétré. Elle est omniprésente pour typer les fonctions de rappel ou les collections.
interface Depot<T> {
parId(id: number): T | undefined;
tous(): T[];
ajouter(item: T): void;
}
interface Produit {
id: number;
nom: string;
}
// On implémente le dépôt spécialisé pour Produit
class DepotProduits implements Depot<Produit> {
private data: Produit[] = [];
parId(id: number) { return this.data.find((p) => p.id === id); }
tous() { return this.data; }
ajouter(p: Produit) { this.data.push(p); }
}
Les types utilitaires reposent sur les generics
TypeScript fournit toute une boîte à outils de types utilitaires, tous construits sur des generics. Les connaître évite de réinventer la roue.
interface Utilisateur {
id: number;
nom: string;
email: string;
}
// Partial<T> : toutes les propriétés deviennent optionnelles
function maj(id: number, champs: Partial<Utilisateur>) { /* ... */ }
maj(1, { nom: "Léa" }); // pas besoin de fournir id ni email
// Pick<T, K> : ne garder que certaines propriétés
type Apercu = Pick<Utilisateur, "id" | "nom">;
// Omit<T, K> : retirer certaines propriétés
type SansId = Omit<Utilisateur, "id">;
// Record<K, V> : construire un objet à clés/valeurs typées
const roles: Record<string, boolean> = { admin: true, invite: false };
// Readonly<T> : rendre l'objet non modifiable
const fige: Readonly<Utilisateur> = { id: 1, nom: "Tom", email: "[email protected]" };
Partial, Pick, Omit, Record, Readonly, Required… tous ces utilitaires ne sont que des generics prédéfinis. En les combinant, on transforme un type existant sans jamais le dupliquer, ce qui garde le code source unique et cohérent.
Bonnes pratiques
Quelques repères pour utiliser les generics sans les rendre illisibles :
- N’introduisez un générique que s’il est réellement réutilisé. Si un type variable n’apparaît qu’à un seul endroit, il ne sert probablement à rien.
- Contraignez avec
extendsdès que possible. UnTtotalement libre limite ce que vous pouvez en faire ; une contrainte débloque l’autocomplétion. - Laissez l’inférence travailler. Précisez
<T>explicitement uniquement quand TypeScript ne peut pas deviner. - Nommez clairement.
Tconvient pour un cas générique, maisTCle/TValeurclarifient les signatures complexes.
En résumé
Les generics permettent d’écrire du code à la fois réutilisable et rigoureusement typé : une seule définition qui s’adapte à chaque type de données, sans jamais retomber dans any. Ils sont la colonne vertébrale des bibliothèques modernes et des types utilitaires de TypeScript. Commencez par des fonctions simples, ajoutez des contraintes extends quand vous en avez besoin, et vous manipulerez bientôt les <T> sans y penser.