Leçon 6 / 8
Leçon 06 · TypeScript

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).

TypeScript
// 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 :

TypeScript
// 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.

TypeScript
// 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.

TypeScript
// 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…

TypeScript
// 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.

TypeScript
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.

TypeScript
// 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"
// À retenir
  • Un générique <T> paramètre une fonction, interface ou classe par un type — réutilisabilité sans sacrifier la sécurité
  • T extends SomeType impose une contrainte : T doit être assignable à ce type
  • interface Box<T> et class 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 T extrait les clés d'un type en union ; typeof val infère le type d'une valeur
  • TypeScript infère T automatiquement dans la plupart des cas — l'annotation explicite est optionnelle