Les génériques TypeScript au quotidien : guide pratique

Frontend2026-09-21TryQuickToolBox

Vous écrivez une fonction qui fonctionne avec n'importe quel type, mais vous ne voulez pas perdre la sécurité de type. Vous pourriez utiliser any, mais cela va à l'encontre de l'objectif de TypeScript. Les génériques vous permettent d'écrire du code qui fonctionne avec plusieurs types tout en préservant les informations de type. Ce guide vous montre comment utiliser les génériques dans votre code quotidien sans trop compliquer les choses.

Que sont les génériques ?

Les génériques sont un moyen de créer des composants réutilisables qui fonctionnent avec une variété de types plutôt qu'un seul. Ils vous permettent de capturer le type de l'entrée et de l'utiliser dans la sortie, assurant ainsi la sécurité de type sans sacrifier la flexibilité.

Pensez aux génériques comme des paramètres de type. Tout comme une fonction prend des paramètres de valeur, une fonction générique prend des paramètres de type. Par exemple, la fonction identité :

function identity<T>(arg: T): T {
  return arg;
}

Ici, T est un paramètre de type. Quand vous appelez identity<string>("hello"), TypeScript déduit que T est string, et le type de retour est string. Si vous passez un nombre, il retourne un nombre. C'est plus précis que d'utiliser any.

Pourquoi ne pas simplement utiliser any ?

Utiliser any désactive la vérification de type. Vous perdez l'autocomplétion, le support de refactoring, et le compilateur ne peut pas détecter les erreurs. Les génériques maintiennent l'information de type à travers votre code, vous offrant à la fois flexibilité et sécurité.

Fonctions génériques en pratique

Regardons un scénario courant : une fonction qui retourne le premier élément d'un tableau.

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

Sans génériques, vous pourriez écrire function firstElement(arr: any[]): any, ce qui perd le type. Avec les génériques, si vous passez un string[], le type de retour est string | undefined.

Vous pouvez également utiliser plusieurs paramètres de type. Par exemple, une fonction qui mappe un tableau vers un nouveau tableau en utilisant un callback :

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

C'est essentiellement ainsi que Array.prototype.map est typé dans la bibliothèque standard de TypeScript.

Interfaces génériques et alias de type

Les génériques ne se limitent pas aux fonctions. Vous pouvez créer des interfaces génériques et des alias de type pour définir des structures de données réutilisables.

interface Box<T> {
  value: T;
}

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

C'est utile pour les réponses d'API, la gestion d'état, ou tout type de conteneur.

Vous pouvez également fournir des types par défaut :

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

Maintenant, ApiResponse sans argument de type utilise unknown par défaut.

Contraintes génériques

Parfois, vous voulez restreindre les types qui peuvent être utilisés avec un générique. Vous pouvez utiliser le mot-clé extends pour ajouter une contrainte.

function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

Maintenant, T doit avoir une propriété length. Vous pouvez appeler getLength("hello") ou getLength([1,2,3]), mais pas getLength(42).

Les contraintes sont puissantes lorsque vous travaillez avec des objets qui partagent une forme commune.

Utiliser les génériques avec les classes

Les classes peuvent aussi être génériques. Un exemple courant est une collection générique :

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const popped = stack.pop(); // number | undefined

Cela garantit que seuls des nombres peuvent être ajoutés à une pile de nombres.

Exemple concret : un émetteur d'événements typé

Construisons un émetteur d'événements simple qui utilise des génériques pour associer les noms d'événements aux types de charge utile.

type EventMap = Record<string, any>;

class EventEmitter<T extends EventMap> {
  private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};

  on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof T>(event: K, payload: T[K]): void {
    this.listeners[event]?.forEach(listener => listener(payload));
  }
}

// Utilisation
interface MyEvents {
  login: { userId: string };
  logout: { userId: string };
}

const emitter = new EventEmitter<MyEvents>();
emitter.on('login', (payload) => {
  console.log(payload.userId); // string
});
emitter.emit('login', { userId: '123' }); // OK
// emitter.emit('login', { userId: 123 }); // Erreur

Ce pattern vous offre une sécurité de type complète pour les noms d'événements et les charges utiles, réduisant les erreurs d'exécution.

Pièges courants et bonnes pratiques

Comparaison : génériques vs any vs types union

Approche Sécurité de type Flexibilité Cas d'utilisation
Génériques Élevée Élevée Composants réutilisables avec types préservés
any Aucune Élevée Prototypage rapide, migration JS
Types union Moyenne Moyenne Ensemble limité de types connus

Les génériques offrent un équilibre entre flexibilité et sécurité, ce qui les rend idéaux pour les bibliothèques et les utilitaires partagés.

FAQ

Quand dois-je utiliser des génériques au lieu des types union ?

Utilisez des génériques lorsque la relation de type entre les entrées et les sorties importe. Les types union sont pour quand une valeur peut être l'un d'un ensemble fixe de types, mais le type spécifique n'a pas besoin d'être préservé à travers les opérations.

Puis-je utiliser des génériques avec des fonctions fléchées ?

Oui, mais la syntaxe peut être délicate dans les fichiers .tsx. Vous pouvez écrire const identity = <T,>(arg: T): T => arg; avec une virgule finale pour éviter l'ambiguïté JSX.

Comment contraindre un générique à n'accepter que des objets ?

Utilisez T extends object ou T extends Record<string, unknown> pour vous assurer que T est un type objet.

Les génériques sont une pierre angulaire du système de types de TypeScript. En les utilisant dans votre code quotidien, vous pouvez écrire des fonctions, des interfaces et des classes à la fois flexibles et sûres. Commencez par des fonctions génériques simples, puis incorporez progressivement des contraintes et des patterns plus complexes selon vos besoins. Pour plus d'outils de développement, consultez notre JSON Formatter pour valider et formater rapidement vos données JSON.