Génériques
Pourquoi les génériques ?
Imagine une fonction qui retourne le premier élément d'un tableau. Si tu l'écris pour number[], elle ne fonctionnera pas pour string[]. Tu pourrais utiliser any, mais tu perdrais toute la sécurité des types.
Les génériques résolvent ce problème : ils permettent d'écrire du code paramétré par un type, comme les paramètres d'une fonction, mais pour les types. Résultat : réutilisabilité maximale, sécurité de type préservée.
- Une seule implémentation pour plusieurs types
- TypeScript infère le type automatiquement à l'appel
- Les erreurs de type sont détectées à la compilation, pas à l'exécution
La règle d'or : utilise any quand le type n'a vraiment pas d'importance. Utilise un générique quand le type doit rester cohérent tout au long de la fonction ou de la classe.
Fonctions génériques
La syntaxe de base utilise un paramètre de type entre chevrons, conventionnellement nommé T (mais n'importe quelle lettre majuscule est valide).
// Sans générique → on perd le type de retour
function identiteAny(arg: any): any {
return arg;
}
// Avec générique → T est inféré à l'appel
function identity<T>(arg: T): T {
return arg;
}
// TypeScript infère T = string
const message = identity("Bonjour"); // type : string
// TypeScript infère T = number
const age = identity(42); // type : number
// Annotation explicite si besoin
const actif = identity<boolean>(true); // type : boolean
Les génériques fonctionnent aussi avec plusieurs paramètres de type :
// Deux paramètres de type : K (clé) et V (valeur)
function creerPaire<K, V>(cle: K, valeur: V): [K, V] {
return [cle, valeur];
}
const paire = creerPaire("nom", 42);
// paire : [string, number]
// Fonction générique sur un tableau
function premier<T>(tableau: T[]): T | undefined {
return tableau[0];
}
const p1 = premier([1, 2, 3]); // type : number | undefined
const p2 = premier(["a", "b"]); // type : string | undefined
Contraintes génériques
Parfois tu veux que T soit au moins d'un certain type. C'est là qu'intervient extends. Il ne crée pas d'héritage — il pose une contrainte sur le type paramètre.
// T doit avoir une propriété length
function longueur<T extends { length: number }>(arg: T): number {
return arg.length;
}
longueur("TypeScript"); // 10 (string a length)
longueur([1, 2, 3]); // 3 (Array a length)
longueur(42); // ❌ Erreur : number n'a pas length
// Contrainte sur un type union
function formaterID<T extends string | number>(id: T): string {
return `ID-${id}`;
}
formaterID(123); // "ID-123"
formaterID("abc"); // "ID-abc"
formaterID(true); // ❌ Erreur : boolean n'est pas string | number
extends dans les génériques ≠ héritage. Quand tu écris T extends string, tu dis que T doit être assignable à string. Quand tu écris class Chien extends Animal, tu crées une hiérarchie de classes. Le mot-clé est le même, le sens est différent.
Interfaces génériques
Les interfaces peuvent aussi être paramétrées par un type. C'est très courant pour modéliser des structures de données réutilisables.
// Interface générique : une boîte qui contient n'importe quel type
interface Box<T> {
contenu: T;
etiquette: string;
}
const boiteNombre: Box<number> = {
contenu: 42,
etiquette: "Réponse ultime"
};
const boiteTexte: Box<string> = {
contenu: "Bonjour",
etiquette: "Message"
};
// Interface générique avec plusieurs paramètres
interface Reponse<T, E = string> {
donnees: T | null;
erreur: E | null;
succes: boolean;
}
const rep: Reponse<{ nom: string; age: number }> = {
donnees: { nom: "Alice", age: 30 },
erreur: null,
succes: true
};
Classes génériques
Une classe générique est idéale pour des structures de données typées : pile, file, liste chaînée…
// Pile générique (Last In, First Out)
class Stack<T> {
private elements: T[] = [];
push(element: T): void {
this.elements.push(element);
}
pop(): T | undefined {
return this.elements.pop();
}
peek(): T | undefined {
return this.elements[this.elements.length - 1];
}
get taille(): number {
return this.elements.length;
}
}
// Pile de nombres
const pileNombres = new Stack<number>();
pileNombres.push(10);
pileNombres.push(20);
console.log(pileNombres.pop()); // 20
// Pile de chaînes
const pileTextes = new Stack<string>();
pileTextes.push("a");
pileTextes.push(42); // ❌ Erreur : number n'est pas string
Types utilitaires intégrés
TypeScript fournit des types utilitaires (utility types) qui sont eux-mêmes des génériques. Ils permettent de transformer des types existants sans les réécrire.
interface Utilisateur {
id: number;
nom: string;
email: string;
age: number;
}
// Partial<T> — toutes les propriétés deviennent optionnelles
type MiseAJour = Partial<Utilisateur>;
// { id?: number; nom?: string; email?: string; age?: number }
// Required<T> — toutes les propriétés deviennent obligatoires
type UtilisateurComplet = Required<Utilisateur>;
// Readonly<T> — toutes les propriétés en lecture seule
type UtilisateurFige = Readonly<Utilisateur>;
const u: UtilisateurFige = { id: 1, nom: "Alice", email: "a@b.fr", age: 30 };
u.nom = "Bob"; // ❌ Erreur : propriété en lecture seule
// Pick<T, K> — ne garder que certaines propriétés
type ProfilPublic = Pick<Utilisateur, "id" | "nom">;
// { id: number; nom: string }
// Omit<T, K> — exclure certaines propriétés
type SansEmail = Omit<Utilisateur, "email">;
// { id: number; nom: string; age: number }
// Record<K, V> — dictionnaire clé/valeur typé
type ScoresJoueurs = Record<string, number>;
const scores: ScoresJoueurs = { alice: 100, bob: 85 };
keyof et typeof dans les génériques
keyof extrait les noms de propriétés d'un type sous forme d'union. Combiné aux génériques, il permet d'écrire des accès à des propriétés 100 % sûrs.
// keyof T : union des clés de l'objet T
function getPropriete<T, K extends keyof T>(obj: T, cle: K): T[K] {
return obj[cle];
}
const user = { nom: "Alice", age: 30, actif: true };
const n = getPropriete(user, "nom"); // type : string
const a = getPropriete(user, "age"); // type : number
const x = getPropriete(user, "adresse"); // ❌ "adresse" n'est pas une clé
// typeof : inférer le type d'une valeur existante
const config = { host: "localhost", port: 3000 };
type Config = typeof config;
// { host: string; port: number }
// Combinaison : clés d'un objet existant
type ClesConfig = keyof typeof config;
// "host" | "port"
- Un générique
<T>paramètre une fonction, interface ou classe par un type — réutilisabilité sans sacrifier la sécurité T extends SomeTypeimpose une contrainte : T doit être assignable à ce typeinterface Box<T>etclass Stack<T>— même syntaxe pour les interfaces et les classes- Types utilitaires :
Partial<T>,Required<T>,Readonly<T>,Pick<T, K>,Omit<T, K>,Record<K, V> keyof Textrait les clés d'un type en union ;typeof valinfère le type d'une valeur- TypeScript infère
Tautomatiquement dans la plupart des cas — l'annotation explicite est optionnelle