frontend / typescript
Génériques
Explication
Ce que vous allez apprendre
- Écrire une fonction générique
<T>dont le type dépend de l'argument reçu - Expliquer pourquoi un générique préserve plus d'information qu'un paramètre
any - Contraindre un générique avec
extendspour limiter les types acceptés - Combiner un générique avec
keyofpour accéder à une propriété d'objet de façon sûre - Utiliser un générique avec plusieurs paramètres de type (
<A, B>)
Dans quel contexte ?
Une équipe écrit une fonction utilitaire premier(tableau: any[]): any qui renvoie le premier élément d'un tableau, utilisée dans toute l'application. Le problème apparaît en code review : premier([1, 2, 3]) renvoie un type any, donc rien n'empêche d'écrire ensuite .toUpperCase() sur ce nombre sans que TypeScript ne s'en plaigne, une erreur qui n'explosera qu'à l'exécution. En réécrivant la fonction en premier<T>(tableau: T[]): T, TypeScript sait que premier([1, 2, 3]) renvoie précisément un number, et bloque immédiatement tout usage incohérent à la compilation.
Écrire du code qui s'adapte à n'importe quel type
Imaginez une boîte qui peut contenir n'importe quoi — des livres, des outils, des vêtements — mais qui garde toujours la trace précise de ce qu'elle contient. C'est exactement ce que sont les génériques en TypeScript : une façon d'écrire une fonction, une interface ou une classe qui fonctionne avec n'importe quel type, tout en conservant l'information exacte de ce type d'un bout à l'autre du programme.
Prérequis
Cette leçon prolonge directement les fonctions typées (leçon 4) et s'appuie sur any/unknown (leçon 2) pour expliquer pourquoi les génériques sont presque toujours préférables à any dans du code réutilisable.
Pourquoi ne pas utiliser any à la place
On pourrait être tenté de remplacer un générique par any pour accepter tout. Mais any fait perdre toute information : une fonction identite(valeur: any): any ne garantit plus que le type d'entrée correspond au type de sortie. Un générique identite<T>(valeur: T): T garde ce lien précis — c'est toute la différence entre accepter n'importe quoi sans contrôle et s'adapter à n'importe quel type en gardant le contrôle.
| Approche | Type d'entrée connu en sortie ? | Sécurité |
|---|---|---|
any | Non, information perdue | Aucune |
unknown | Non, doit être vérifié explicitement | Sûre mais rigide |
Générique <T> | Oui, préservé automatiquement | Sûre et flexible |
Piège fréquent
Remplacer un générique par any "pour aller plus vite" fait perdre tout l'intérêt du typage : function f(x: any): any accepte n'importe quoi en entrée ET en sortie, sans aucune garantie de cohérence entre les deux, contrairement à function f<T>(x: T): T.
Contraindre un générique
Un générique peut être limité à une famille de types via extends : par exemple, exiger que le type accepté possède une propriété length. Cela permet d'écrire du code réutilisable sans renoncer à la sécurité de typage.
Le duo génériques et keyof
Combiné à keyof, un générique permet d'accéder dynamiquement à une propriété d'un objet tout en garantissant, à la compilation, que cette propriété existe réellement. C'est l'un des patterns les plus utiles du langage, que vous retrouverez constamment dans du code TypeScript professionnel, notamment dans les prochaines leçons sur les unions et le narrowing.
Commandes & code
Génériques
// Fonction générique : le type est déterminé à l'appel
function identite<T>(valeur: T): T {
return valeur;
}
identite<string>("hello");
identite(42); // T inféré automatiquement = number
// Générique avec plusieurs paramètres
function paire<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
// Contrainte générique : T DOIT avoir une propriété length
interface AvecLongueur {
length: number;
}
function logLongueur<T extends AvecLongueur>(valeur: T): T {
console.log(valeur.length);
return valeur;
}
logLongueur("bonjour"); // OK, string a .length
logLongueur([1, 2, 3]); // OK, array a .length
// logLongueur(42); // Error : number n'a pas .length
// Valeur par défaut pour un paramètre générique
interface ReponseApi<T = unknown> {
data: T;
status: number;
}
const reponse: ReponseApi<string[]> = { data: ["a", "b"], status: 200 };
// keyof + générique : accès sécurisé à une propriété par son nom
function getProp<T, K extends keyof T>(objet: T, cle: K): T[K] {
return objet[cle];
}
const utilisateur = { id: 1, nom: "Ada" };
const nom = getProp(utilisateur, "nom"); // typé string
// getProp(utilisateur, "email"); // Error : "email" n'existe pas sur le type
// Classe générique
class Pile<T> {
private elements: T[] = [];
empiler(item: T): void {
this.elements.push(item);
}
depiler(): T | undefined {
return this.elements.pop();
}
}
const pileNombres = new Pile<number>();
pileNombres.empiler(1);
// Générique conditionnel simple, utile pour des helpers d'API
type ExtraireData<T> = T extends { data: infer D } ? D : never;
type DonneesUtilisateur = ExtraireData<ReponseApi<{ id: number }>>; // { id: number }Résumé
<T extends ...>contraint un générique à un sous-ensemble de types acceptables.keyof T+ un second génériqueK extends keyof Tsécurise l'accès dynamique aux propriétés.- Les génériques évitent la duplication de code tout en gardant une inférence précise.
Exercices pratiques
Mission : sauver premier() de any
Objectif : Remplacer une fonction utilitaire typée any par un générique préservant l'information de type, puis sécuriser un accès dynamique aux propriétés.
Contexte
Une fonction utilitaire function premier(tableau: any[]): any { return tableau[0]; } est utilisée partout dans l'application. En code review, quelqu'un remarque que premier([1, 2, 3]).toUpperCase() compile sans la moindre erreur, alors que 1 n'a pas de méthode toUpperCase. Ta mission : comprendre le problème et le réparer avec un générique.