Retour au blog
janv. 2026·3 min de lectureGénéré par IA
Langue:DeutschEnglishFrançaisÖsterreichischKlingonian Version originale: English

Comprendre les génériques TypeScript

#typescript#javascript

Les génériques sont la fonctionnalité qui fait trébucher juste après qu'on s'est senti à l'aise avec les types TypeScript de base, puis qui, une fois le déclic venu, devient l'un des outils les plus utilisés de la boîte. L'idée centrale est modeste : au lieu d'écrire une fonction ou un type pour un type précis, on l'écrit pour « le type qui sera passé », et TypeScript suit ce type pour vous dans le reste de la signature.

Le problème que résolvent les génériques

Supposons que vous vouliez une fonction qui renvoie le premier élément d'un tableau.

function firstOfNumbers(arr: number[]): number {
  return arr[0];
}

function firstOfStrings(arr: string[]): string {
  return arr[0];
}

Deux fonctions, une logique identique, des types différents. On pourrait la typer avec any :

function first(arr: any[]): any {
  return arr[0];
}

Cela marche, mais vous avez jeté toute l'information. first([1, 2, 3]) est désormais de type any au lieu de number, donc rien en aval ne détecte une erreur comme first([1, 2, 3]).toUpperCase().

Les génériques conservent le lien entre le type d'entrée et le type de sortie :

function first<T>(arr: T[]): T {
  return arr[0];
}

const n = first([1, 2, 3]);       // T inferred as number, n: number
const s = first(['a', 'b', 'c']); // T inferred as string, s: string

T est un paramètre de type : un espace réservé que TypeScript remplit selon ce que vous passez réellement, comme un paramètre de fonction est rempli par une valeur. Vous n'avez pas eu à écrire first<number>([1, 2, 3]) : TypeScript infère T à partir de l'argument.

Contraindre ce que T peut être

Un T sans contrainte peut être n'importe quoi, ce qui signifie qu'on ne peut pas supposer une forme particulière. Ceci ne compilera pas :

function getLength<T>(item: T): number {
  return item.length; // Error: Property 'length' does not exist on type 'T'
}

TypeScript a raison de se plaindre : T pourrait être number, qui n'a pas de .length. Contraignez-le avec extends :

function getLength<T extends { length: number }>(item: T): number {
  return item.length; // OK — T is guaranteed to have a `length` property
}

getLength('hello');      // OK, strings have .length
getLength([1, 2, 3]);    // OK, arrays have .length
getLength(42);           // Error: number doesn't satisfy the constraint

Ici, extends ne désigne pas l'héritage de classe : cela signifie « T doit être assignable à cette forme ». Tout type ayant une propriété numérique .length la satisfait.

Interfaces et classes génériques

Les génériques ne servent pas qu'aux fonctions : ils paramètrent les types eux-mêmes. Un Box<T> qui peut contenir n'importe quelle valeur unique :

interface Box<T> {
  value: T;
}

const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: 'hello' };

Un exemple plus réaliste : un wrapper de réponse d'API typé, utilisé dans toute une application :

interface ApiResponse<T> {
  data: T;
  status: number;
  error: string | null;
}

async function fetchUser(id: string): Promise<ApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json();
  return { data, status: res.status, error: null };
}

Tout appelant de fetchUser obtient response.data typé User, et non any : le générique transporte le type de charge utile spécifique à travers toute la forme de la réponse, sans qu'il faille écrire un UserApiResponse, un PostApiResponse, etc. pour chaque entité.

Plusieurs paramètres de type

Les génériques peuvent prendre plus d'un paramètre, ce qui est courant pour les fonctions qui combinent deux éléments indépendants : la fonction map en est l'exemple canonique :

function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

const lengths = mapArray(['a', 'bb', 'ccc'], (s) => s.length);
// T inferred as string, U inferred as number
// lengths: number[]

T est le type de l'élément d'entrée, U est ce que renvoie le callback : ils n'ont pas à coïncider, et TypeScript infère les deux indépendamment à partir de la façon dont vous l'appelez.

Paramètres de type par défaut

Comme les paramètres de fonction par défaut, les génériques peuvent avoir des valeurs par défaut, utiles quand un paramètre de type est généralement le même mais doit rester modifiable :

interface PaginatedResponse<T = unknown> {
  items: T[];
  page: number;
  totalPages: number;
}

function emptyPage(): PaginatedResponse {
  return { items: [], page: 1, totalPages: 1 }; // T defaults to unknown
}

function usersPage(): PaginatedResponse<User> {
  return { items: [], page: 1, totalPages: 1 }; // T explicitly User
}

Une erreur courante : recourir trop tôt aux génériques

Toute fonction avec un paramètre n'a pas besoin d'être générique. Si une fonction n'opère jamais que sur string, écrire function process<T extends string>(input: T) ajoute un paramètre de type qui ne varie jamais et ne justifie jamais sa complexité. Les génériques valent leur coût quand la relation entre le type d'entrée et le type de sortie doit réellement être préservée au cours d'un appel : un first<T> qui renvoie le même type d'élément que celui qu'il a reçu, un Box<T> qui enveloppe des valeurs quelconques, un mapArray<T, U> qui transforme un type en un autre. Si le type ne change jamais d'un usage à l'autre, un type concret est plus clair qu'un générique que personne n'instancie différemment.

À retenir

Les génériques existent pour préserver une relation entre types que any effacerait et que l'écriture de N surcharges quasi identiques dupliquerait. Le modèle mental qui fait le déclic : un paramètre de type est un paramètre, comme un argument de fonction, sauf que ce que vous passez est un type, et TypeScript le suit dans le reste de la signature comme il suit une valeur.

Plus récentFeature flags : un guide pratiquePlus ancienConstruire un cache LRU à partir de zéro