Si vous écrivez TypeScript depuis un moment mais que les génériques vous semblent encore être quelque chose
que vous copiez-collez depuis Stack Overflow sans vraiment comprendre, cet article est fait pour vous.
Les génériques sont la fonctionnalité qui fait passer TypeScript de "JavaScript avec des annotations de type" à un
système de types véritablement expressif. Une fois que ça clique, vous les verrez partout — et vous les utiliserez
naturellement au lieu de vous rabattre sur any quand les choses se compliquent. Le
chapitre sur les génériques du Manuel TypeScript
couvre la spécification complète ; cet article se concentre sur les modèles que vous utiliserez réellement dans de vraies bases de code.
Le problème avec any
Les génériques existent pour résoudre un problème précis : vous voulez qu'une fonction ou une structure de données
fonctionne avec plusieurs types, mais vous ne voulez pas perdre les informations de type dans le processus.
La solution naïve est any — et elle semble correcte jusqu'à ce que vous réalisiez ce que vous avez perdu :
// With any — TypeScript has no idea what comes out
function identity(arg: any): any {
return arg;
}
const result = identity('hello');
// result is typed as 'any' — you've lost the string type
// TypeScript won't catch this:
result.toFixed(2); // no error at compile time, crashes at runtimeL'information de type entre, mais elle ne sort pas. Tout ce que vous faites avec
result est totalement non vérifié. Ce n'est pas une base de code TypeScript — c'est du JavaScript
avec des étapes supplémentaires. Les génériques résolvent cela en vous permettant de dire : "Je ne sais pas encore le type exact, mais
ce qui entre devrait ressortir avec le même type."
// With a generic — T flows through the function
function identity<T>(arg: T): T {
return arg;
}
const result = identity('hello');
// result is typed as 'string' ✅
result.toFixed(2); // ❌ TypeScript catches this: Property 'toFixed' does not exist on type 'string'
const count = identity(42);
// count is typed as 'number' ✅Le <T> déclare un paramètre de type — pensez-y comme une variable pour
les types. TypeScript déduit ce qu'est T à partir de l'argument que vous passez, donc vous n'avez presque
jamais besoin de l'écrire explicitement. identity<string>('hello') fonctionne, mais
identity('hello') aussi — TypeScript le comprend.
Fonctions génériques en pratique
La fonction identity est l'exemple d'enseignement canonique mais pas quelque chose que vous écririez en production. Voici les types de fonctions génériques qui apparaissent réellement dans de vraies bases de code :
// A type-safe array first/last helper
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
const firstUser = first(users); // UserProfile | undefined
const lastOrder = last(orders); // Order | undefined
const firstNum = first([1, 2, 3]); // number | undefined
// Group array items by a key
function groupBy<T, K extends string | number>(
items: T[],
getKey: (item: T) => K
): Record<K, T[]> {
return items.reduce((acc, item) => {
const key = getKey(item);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {} as Record<K, T[]>);
}
// TypeScript infers T as Order and K as string
const byStatus = groupBy(orders, order => order.status);
// byStatus: Record<string, Order[]>
// byStatus['pending'] is Order[] ✅Remarquez comment TypeScript déduit T et K automatiquement selon comment
vous appelez la fonction. Vous obtenez une sécurité de type complète sur la valeur de retour sans écrire une seule
annotation de type explicite au niveau de l'appel. C'est le bénéfice.
Interfaces et types génériques
C'est là que les génériques deviennent indispensables pour le travail quotidien. Chaque base de code qui communique avec une API finit par avoir besoin d'une poignée de types enveloppants génériques. Voici ceux que vous verrez (et écrirez) constamment :
// API response wrapper — used across every endpoint
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
// Paginated list response
interface PaginatedResult<T> {
items: T[];
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
}
// Using them with concrete types
interface UserProfile {
id: number;
name: string;
email: string;
avatarUrl: string;
}
interface Order {
id: number;
userId: number;
total: number;
status: 'pending' | 'shipped' | 'delivered';
}
// The return types are fully typed — no casting needed downstream
async function getUser(id: number): Promise<ApiResponse<UserProfile>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function listOrders(page: number): Promise<PaginatedResult<Order>> {
const res = await fetch(`/api/orders?page=${page}`);
return res.json();
}
const response = await getUser(1);
response.data.email; // ✅ string
response.data.avatarUrl; // ✅ string
const result = await listOrders(1);
result.items[0].status; // ✅ 'pending' | 'shipped' | 'delivered'
result.totalPages; // ✅ numberUserProfile, Order) automatiquement à partir d'un exemple de réponse. Vous
les branchez ensuite dans vos wrappers génériques.Contraintes génériques
Parfois vous voulez accepter plusieurs types, mais pas absolument n'importe quel type. Les
contraintes génériques vous permettent de spécifier ce qu'un paramètre de type doit avoir. La syntaxe est
T extends SomeType — ce qui signifie "T doit être assignable à SomeType" :
// Without constraint — TypeScript doesn't know T has an id property
function findById<T>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id); // ❌ Property 'id' does not exist on type 'T'
}
// With constraint — T must have at least an id: number field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id); // ✅
}
// Works with any type that has id: number
const user = findById(users, 42); // UserProfile | undefined
const order = findById(orders, 101); // Order | undefined
// Another common constraint — T must be an object (excludes primitives)
function mergeDefaults<T extends object>(partial: Partial<T>, defaults: T): T {
return { ...defaults, ...partial };
}
// T must have a name: string and email: string
function formatContact<T extends { name: string; email: string }>(contact: T): string {
return `${contact.name} <${contact.email}>`;
}
// Works on any object with those two fields — UserProfile, Employee, whatever
formatContact({ name: 'Alice', email: '[email protected]', role: 'admin' }); // ✅La contrainte ne verrouille pas T exactement sur { id: number } —
cela signifie que T doit avoir au moins cette forme. Donc passer un complet
UserProfile avec dix champs est bien. C'est le typage structurel en action, qui est
l'une des fonctionnalités les plus puissantes et
bien documentées
de TypeScript.
La contrainte keyof
L'un des modèles génériques les plus utiles dans la bibliothèque standard TypeScript et les vraies
bases de code est la combinaison des génériques avec keyof. Il vous permet d'écrire des fonctions qui
acceptent un nom de propriété d'un type d'objet et garantissent que le type de retour correspond à cette propriété.
// keyof T is the union of all keys of T as string literals
// K extends keyof T means K must be one of those keys
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: UserProfile = {
id: 1,
name: 'Alice',
email: '[email protected]',
avatarUrl: 'https://example.com/avatar.png'
};
const name = getProperty(user, 'name'); // string ✅
const id = getProperty(user, 'id'); // number ✅
const email = getProperty(user, 'email'); // string ✅
getProperty(user, 'password'); // ❌ Argument of type '"password"' is not assignable to
// parameter of type 'keyof UserProfile'
// Real use case: a generic sort function
function sortBy<T>(items: T[], key: keyof T): T[] {
return [...items].sort((a, b) => {
const av = a[key];
const bv = b[key];
return av < bv ? -1 : av > bv ? 1 : 0;
});
}
const byName = sortBy(users, 'name'); // ✅ sorted by name
const byId = sortBy(users, 'id'); // ✅ sorted by id
sortBy(users, 'nonexistent'); // ❌ caught at compile timeParamètres de type par défaut
Tout comme les paramètres de fonction peuvent avoir des valeurs par défaut, les paramètres de type le peuvent aussi. C'est utile quand vous avez un type générique presque toujours utilisé avec un type spécifique, mais que vous voulez conserver la flexibilité pour les cas où ce n'est pas le cas.
// Default type parameter — T defaults to string if not specified
interface Cache<T = string> {
get(key: string): T | undefined;
set(key: string, value: T, ttlMs?: number): void;
delete(key: string): void;
clear(): void;
}
// Without specifying T — uses the default (string)
declare const stringCache: Cache;
const val = stringCache.get('theme'); // string | undefined
// Specifying a different T
declare const userCache: Cache<UserProfile>;
const user2 = userCache.get('user:42'); // UserProfile | undefined
// Another useful example: an event emitter with a typed payload
interface TypedEvent<TPayload = void> {
subscribe(handler: (payload: TPayload) => void): () => void;
emit(payload: TPayload): void;
}
// Events with no payload — default void keeps the API clean
const appReady: TypedEvent = { /* ... */ };
appReady.emit(); // no argument needed
// Events with a payload
const userLoggedIn: TypedEvent<{ userId: number; timestamp: Date }> = { /* ... */ };
userLoggedIn.emit({ userId: 42, timestamp: new Date() });Types utilitaires génériques intégrés
TypeScript est livré avec un ensemble de types utilitaires intégrés qui sont tous implémentés en utilisant les génériques que vous avez appris ci-dessus. Comprendre comment les utiliser est l'une des compétences TypeScript les plus pratiques que vous puissiez avoir. Voici l'ensemble de base avec de vrais exemples montrant le type de sortie :
interface UserProfile {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'viewer';
passwordHash: string;
createdAt: Date;
}
// Partial<T> — all fields become optional (great for PATCH payloads)
type UpdateUserPayload = Partial<UserProfile>;
// { id?: number; name?: string; email?: string; role?: ...; ... }
// Required<T> — all fields become required (reverse of Partial)
interface DraftConfig {
apiUrl?: string;
timeout?: number;
maxRetries?: number;
}
type ResolvedConfig = Required<DraftConfig>;
// { apiUrl: string; timeout: number; maxRetries: number }
// Pick<T, K> — keep only the named fields
type UserSummary = Pick<UserProfile, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }
// Use in list views — only ship what the UI needs
// Omit<T, K> — exclude the named fields
type PublicUserProfile = Omit<UserProfile, 'passwordHash'>;
// { id: number; name: string; email: string; role: ...; createdAt: Date }
// Safe to include in API responses
// Record<K, V> — typed dictionary / map
type PermissionMap = Record<UserProfile['role'], string[]>;
// { admin: string[]; user: string[]; viewer: string[] }
const permissions: PermissionMap = {
admin: ['read', 'write', 'delete', 'admin'],
user: ['read', 'write'],
viewer: ['read']
};
// ReturnType<T> — infer what a function returns (keeps types in sync automatically)
function buildUserSession(user: UserProfile) {
return {
token: crypto.randomUUID(),
userId: user.id,
role: user.role,
expiresAt: new Date(Date.now() + 3_600_000)
};
}
type UserSession = ReturnType<typeof buildUserSession>;
// { token: string; userId: number; role: 'admin' | 'user' | 'viewer'; expiresAt: Date }
// Change buildUserSession and UserSession updates automatically ✅Si vous convertissez du JavaScript existant en TypeScript et construisez ces types depuis zéro, l'outil JS vers TypeScript peut vous aider à mettre en place les types initiaux afin que vous puissiez commencer à superposer des types utilitaires dessus.
Un exemple réaliste de bout en bout : Client API typé
Voici le modèle auquel je reviens chaque fois que je construis une couche API typée. Il combine des fonctions génériques, des interfaces génériques et des types utilitaires en quelque chose qui vous donne une sécurité de type de bout en bout depuis l'appel fetch jusqu'au composant qui consomme les données :
// The response envelope — wraps every API response
interface ApiResponse<T> {
data: T;
meta: {
requestId: string;
duration: number;
};
}
// Error envelope — what the API sends on failure
interface ApiError {
code: string;
message: string;
details?: Record<string, string[]>;
}
// The generic fetch wrapper
async function fetchApi<T>(
url: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...options?.headers },
...options
});
if (!res.ok) {
const err: ApiError = await res.json();
throw new Error(`[${err.code}] ${err.message}`);
}
return res.json();
}
// Typed endpoint functions — T is set at each call site
async function getUser(id: number): Promise<UserProfile> {
const { data } = await fetchApi<UserProfile>(`/api/users/${id}`);
return data;
}
async function listOrders(userId: number, page = 1): Promise<PaginatedResult<Order>> {
const { data } = await fetchApi<PaginatedResult<Order>>(
`/api/users/${userId}/orders?page=${page}`
);
return data;
}
async function createOrder(
payload: Pick<Order, 'userId' | 'total'>
): Promise<Order> {
const { data } = await fetchApi<Order>('/api/orders', {
method: 'POST',
body: JSON.stringify(payload)
});
return data;
}
// Usage — fully typed, no casting anywhere
const user = await getUser(42);
console.log(user.email); // string ✅
const orders = await listOrders(42);
orders.items[0].status; // 'pending' | 'shipped' | 'delivered' ✅
orders.totalPages; // number ✅
const newOrder = await createOrder({ userId: 42, total: 99.99 });
newOrder.id; // number ✅Erreurs courantes
Quelques modèles à éviter une fois que vous êtes à l'aise avec les génériques :
- Utiliser
anyà l'intérieur d'un générique. Si vous écrivezfunction wrap<T>(val: T): any, vous avez annulé l'objectif. Tout le point est que le type traverse — utiliseranycomme type de retour ou à l'intérieur de l'implémentation signifie que TypeScript ne peut plus le suivre. - Sur-contraindre avec des types spécifiques. Écrire
function process<T extends UserProfile>quand vous n'avez besoin que deT extends { id: number }est trop restrictif. Utilisez la contrainte minimale qui fait fonctionner votre implémentation — ainsi la fonction reste réutilisable entre différents types qui ont la même forme. - Opter pour les génériques quand une union suffirait. Si une fonction prend soit une
stringsoit unnumberet la logique est différente pour chacun,function f(arg: string | number)avec une garde de type à l'intérieur est plus propre qu'un générique. Les génériques brillent quand la même logique s'applique à toutes les variantes de type. - Trop de paramètres de type. Si vous vous retrouvez à écrire
<T, U, V, W>, prenez du recul. C'est généralement signe que la fonction fait trop de choses, ou que les types pourraient être exprimés comme une seule interface. Le code source du compilateur TypeScript lui-même est une bonne référence — même les utilitaires complexes ont rarement besoin de plus de 2–3 paramètres de type.
Pour conclure
Les génériques sont le point où TypeScript cesse d'être du "JavaScript annoté" et commence
à être un système de types véritablement puissant. L'idée centrale est simple : capturer le type comme paramètre
afin qu'il traverse votre fonction ou interface sans perdre d'information. À partir de là, les contraintes
vous permettent de réduire ce qui est acceptable, keyof vous donne un accès aux propriétés sûr sur le plan des types, et
les types utilitaires intégrés (Partial, Pick, Omit,
Record) gèrent les modèles que vous écririez autrement à la main. La meilleure prochaine étape
est d'ouvrir un vrai projet, trouver une fonction où vous avez utilisé any parce que vous ne connaissiez pas une autre façon,
et la remplacer par un générique. Le
Manuel TypeScript sur les génériques
et la
référence des types utilitaires
sont les deux pages qui méritent d'être mises en favoris. Et si vous construisez des interfaces à partir de vraies charges utiles d'API,
JSON vers TypeScript peut générer les types concrets —
ensuite il suffit de les envelopper dans vos types utilitaires génériques comme
ApiResponse<T> et PaginatedResult<T> pour obtenir une
sécurité de type de bout en bout.