Les génériques TypeScript au quotidien : guide pratique
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
- Ne pas abuser des génériques. Si une fonction ne fonctionne qu'avec un type spécifique, ne la rendez pas générique.
- Utilisez des noms de paramètres de type descriptifs.
Tconvient pour les cas simples, maisTItemouTResponsepeuvent être plus clairs. - Préférez les contraintes pour éviter les erreurs d'exécution. Si vous avez besoin d'une propriété, ajoutez une contrainte.
- Tirez parti de l'inférence de type. Souvent, vous n'avez pas besoin de passer explicitement des arguments de type ; TypeScript peut les déduire.
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.