Génériques TypeScript pour le code quotidien : guide pratique
Vous écrivez une fonction qui fonctionne avec n'importe quel type, mais TypeScript continue de se plaindre à propos de any. Ou vous avez copié la même fonction trois fois juste pour gérer différents types. Les génériques résolvent ce problème—ils vous permettent d'écrire du code qui fonctionne avec plusieurs types tout en préservant la sécurité de type. Mais beaucoup de développeurs les évitent ou les surutilisent. Ce guide vous montre comment utiliser les génériques dans le code quotidien sans prise de tête.
Que sont les génériques et pourquoi s'y intéresser ?
Les génériques sont un moyen de paramétrer les types. Au lieu de coder en dur un type comme string ou number, vous utilisez une variable de type (souvent T) qui est remplacée par un type concret lorsque la fonction ou la classe est utilisée.
Pourquoi est-ce important ? Parce que cela vous permet d'écrire du code réutilisable qui reste typé. Sans génériques, vous devriez soit dupliquer le code, soit utiliser any, ce qui va à l'encontre du but de TypeScript.
Fonctions génériques : la brique de base
Commençons par un exemple simple. Supposons que vous voulez une fonction qui retourne le premier élément d'un tableau.
function firstElement(arr: any[]): any {
return arr[0];
}
Cela fonctionne, mais vous perdez l'information de type. Si vous passez un tableau de chaînes, le type de retour est any, pas string. Maintenant avec les génériques :
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
Maintenant TypeScript sait que si vous passez un string[], vous obtenez un string | undefined en retour. Le type est préservé.
Vous pouvez aussi utiliser plusieurs paramètres de type :
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
Interfaces et alias de type génériques
Les génériques ne se limitent pas aux fonctions. Vous pouvez créer des interfaces et des alias de type génériques pour décrire des structures de données flexibles.
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
C'est particulièrement utile pour les réponses d'API. Au lieu de définir une nouvelle interface pour chaque point de terminaison, vous pouvez définir un ApiResponse<T> générique :
interface ApiResponse<T> {
data: T;
status: number;
}
type User = { id: number; name: string };
const response: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
};
Contraintes génériques : ajouter des règles
Parfois, vous voulez restreindre les types qui peuvent être utilisés avec votre générique. C'est là que les contraintes entrent en jeu. Utilisez le mot-clé extends pour exiger que le type ait certaines propriétés.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(42); // Erreur : number n'a pas de length
Les contraintes rendent vos génériques plus prévisibles et évitent les erreurs d'exécution.
Classes génériques
Les classes peuvent aussi être génériques. C'est pratique pour des structures de données comme les piles, les files d'attente ou les dépôts.
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined
Quand utiliser les génériques (et quand ne pas le faire)
Les génériques sont puissants, mais ils peuvent rendre le code plus difficile à lire s'ils sont surutilisés. Voici un guide rapide :
| Utilisez les génériques quand | Évitez les génériques quand |
|---|---|
| Vous devez préserver les relations de type | Un simple type union ou any suffit |
| Vous écrivez des utilitaires réutilisables | Le type est toujours le même |
| Vous voulez éviter la duplication de code | Cela rend le code plus difficile à comprendre |
Une bonne règle empirique : si vous vous surprenez à écrire la même logique pour plusieurs types, envisagez les génériques. Si vous ajoutez un troisième paramètre de type et que personne ne le comprend, faites marche arrière.
Modèles courants et bonnes pratiques
- Utilisez des noms descriptifs pour les paramètres de type.
Tconvient pour les cas simples, maisTDataouTResponsepeuvent être plus clairs. - Préférez les contraintes à
any. Si vous avez besoin de flexibilité, utilisezextendspour imposer une forme minimale. - Tirez parti des paramètres de type par défaut. Vous pouvez fournir une valeur par défaut :
<T = string>. - Utilisez les génériques avec les types utilitaires. Les types intégrés de TypeScript
Partial<T>,Readonly<T>etRecord<K, V>sont tous génériques.
Exemple concret : un client API générique
Rassemblons le tout avec un exemple pratique : un client API simple qui récupère des données et renvoie une réponse typée.
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function main() {
const post = await fetchData<Post>("https://jsonplaceholder.typicode.com/posts/1");
console.log(post.title); // string
}
Ce modèle est courant dans le code frontend. Vous obtenez la sécurité de type sans vous répéter pour chaque point de terminaison.
FAQ
Que signifie la syntaxe <T> en TypeScript ?
<T> déclare un paramètre de type. C'est un espace réservé pour un type qui sera spécifié lorsque la fonction, l'interface ou la classe est utilisée. Vous pouvez le nommer comme vous voulez, mais T est conventionnel pour un type unique.
Puis-je utiliser plusieurs paramètres de type génériques ?
Oui. Vous pouvez en définir autant que nécessaire, séparés par des virgules : <T, U, V>. Utilisez des noms descriptifs pour garder votre code lisible.
Comment contraindre un générique à n'accepter que des objets ?
Utilisez T extends object. Cela garantit que T est un type objet et non une primitive comme string ou number. Vous pouvez aussi contraindre à des formes spécifiques, comme T extends { id: number }.
Les génériques sont un élément essentiel pour écrire du TypeScript maintenable. Si vous travaillez avec des données JSON et souhaitez les valider ou les formater rapidement, essayez notre JSON Formatter pour embellir et inspecter vos réponses d'API.