Génériques
Code réutilisable avec la sécurité des types
10-15 min
Génériques en TypeScript
Les génériques permettent de créer des composants réutilisables qui peuvent fonctionner avec différents types tout en conservant la sécurité des types.
Introduction aux génériques
Problème sans génériques
// Sans génériques - code dupliqué
function identiteString(arg: string): string {
return arg;
}
function identiteNumber(arg: number): number {
return arg;
}
function identiteBoolean(arg: boolean): boolean {
return arg;
}
// Ou utilisation d'any - perte de type safety
function identiteAny(arg: any): any {
return arg;
}
Solution avec génériques
// Avec génériques - réutilisable et type-safe
function identite<T>(arg: T): T {
return arg;
}
// Utilisation
let resultatString = identite<string>("hello"); // Type: string
let resultatNumber = identite<number>(42); // Type: number
let resultatBoolean = identite<boolean>(true); // Type: boolean
// Inférence de type
let resultat1 = identite("hello"); // TypeScript infère T = string
let resultat2 = identite(42); // TypeScript infère T = number
Fonctions génériques
Syntaxe de base
// Fonction générique simple
function premierElement<T>(tableau: T[]): T | undefined {
return tableau[0];
}
let nombres = [1, 2, 3, 4, 5];
let premier = premierElement(nombres); // Type: number | undefined
let mots = ["hello", "world"];
let premierMot = premierElement(mots); // Type: string | undefined
Plusieurs paramètres génériques
function echanger<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
let paire = echanger(["hello", 42]); // Type: [number, string]
let autrePaire = echanger([true, "world"]); // Type: [string, boolean]
Fonctions avec contraintes
interface AvecLongueur {
length: number;
}
function afficherLongueur<T extends AvecLongueur>(arg: T): T {
console.log(`Longueur: ${arg.length}`);
return arg;
}
afficherLongueur("hello"); // OK - string a une propriété length
afficherLongueur([1, 2, 3]); // OK - array a une propriété length
afficherLongueur({ length: 10 }); // OK - objet avec propriété length
// afficherLongueur(42); // ❌ Erreur - number n'a pas de propriété length
Interfaces génériques
Interface générique de base
interface Reponse<T> {
succes: boolean;
donnees?: T;
erreur?: string;
timestamp: Date;
}
// Utilisation avec différents types
interface Utilisateur {
id: number;
nom: string;
email: string;
}
interface Produit {
id: number;
nom: string;
prix: number;
}
let reponseUtilisateur: Reponse<Utilisateur> = {
succes: true,
donnees: { id: 1, nom: "Alice", email: "alice@example.com" },
timestamp: new Date()
};
let reponseProduits: Reponse<Produit[]> = {
succes: true,
donnees: [
{ id: 1, nom: "Ordinateur", prix: 999 },
{ id: 2, nom: "Souris", prix: 25 }
],
timestamp: new Date()
};
Interface avec contraintes
interface Comparable<T> {
comparerA(autre: T): number;
}
interface Triable<T extends Comparable<T>> {
elements: T[];
trier(): void;
}
class Nombre implements Comparable<Nombre> {
constructor(private valeur: number) {}
comparerA(autre: Nombre): number {
return this.valeur - autre.valeur;
}
obtenirValeur(): number {
return this.valeur;
}
}
class ListeTriable<T extends Comparable<T>> implements Triable<T> {
elements: T[] = [];
ajouter(element: T): void {
this.elements.push(element);
}
trier(): void {
this.elements.sort((a, b) => a.comparerA(b));
}
}
// Utilisation
let listeNombres = new ListeTriable<Nombre>();
listeNombres.ajouter(new Nombre(3));
listeNombres.ajouter(new Nombre(1));
listeNombres.ajouter(new Nombre(2));
listeNombres.trier();
Classes génériques
Classe générique simple
class Conteneur<T> {
private contenu: T;
constructor(valeur: T) {
this.contenu = valeur;
}
obtenirContenu(): T {
return this.contenu;
}
definirContenu(valeur: T): void {
this.contenu = valeur;
}
transformer<U>(transformateur: (valeur: T) => U): Conteneur<U> {
return new Conteneur(transformateur(this.contenu));
}
}
// Utilisation
let conteneurString = new Conteneur("hello");
let conteneurNumber = conteneurString.transformer(s => s.length);
console.log(conteneurNumber.obtenirContenu()); // 5
Classe avec plusieurs génériques
class Paire<T, U> {
constructor(
private premier: T,
private second: U
) {}
obtenirPremier(): T {
return this.premier;
}
obtenirSecond(): U {
return this.second;
}
echanger(): Paire<U, T> {
return new Paire(this.second, this.premier);
}
transformer<V, W>(
transformateurPremier: (valeur: T) => V,
transformateurSecond: (valeur: U) => W
): Paire<V, W> {
return new Paire(
transformateurPremier(this.premier),
transformateurSecond(this.second)
);
}
}
// Utilisation
let paire = new Paire("hello", 42);
let paireEchangee = paire.echanger(); // Type: Paire<number, string>
let paireTransformee = paire.transformer(
s => s.length,
n => n.toString()
); // Type: Paire<number, string>
Contraintes génériques avancées
Contrainte keyof
function obtenirPropriete<T, K extends keyof T>(objet: T, cle: K): T[K] {
return objet[cle];
}
interface Personne {
nom: string;
age: number;
email: string;
}
let personne: Personne = {
nom: "Alice",
age: 30,
email: "alice@example.com"
};
let nom = obtenirPropriete(personne, "nom"); // Type: string
let age = obtenirPropriete(personne, "age"); // Type: number
// let invalide = obtenirPropriete(personne, "invalide"); // ❌ Erreur
Contraintes conditionnelles
type EstTableau<T> = T extends any[] ? true : false;
type Test1 = EstTableau<string[]>; // true
type Test2 = EstTableau<string>; // false
type Test3 = EstTableau<number[]>; // true
// Type conditionnel plus complexe
type ElementType<T> = T extends (infer U)[] ? U : T;
type Test4 = ElementType<string[]>; // string
type Test5 = ElementType<number>; // number
type Test6 = ElementType<boolean[]>; // boolean
Contraintes avec types mappés
type Optionnel<T> = {
[K in keyof T]?: T[K];
};
type Requis<T> = {
[K in keyof T]-?: T[K];
};
type LectureSeule<T> = {
readonly [K in keyof T]: T[K];
};
interface Utilisateur {
id: number;
nom: string;
email: string;
age?: number;
}
type UtilisateurOptionnel = Optionnel<Utilisateur>;
// { id?: number; nom?: string; email?: string; age?: number; }
type UtilisateurRequis = Requis<Utilisateur>;
// { id: number; nom: string; email: string; age: number; }
type UtilisateurLectureSeule = LectureSeule<Utilisateur>;
// { readonly id: number; readonly nom: string; readonly email: string; readonly age?: number; }
Types utilitaires génériques
Types utilitaires intégrés
interface Produit {
id: number;
nom: string;
prix: number;
description: string;
enStock: boolean;
}
// Partial - toutes les propriétés optionnelles
type ProduitPartiel = Partial<Produit>;
// Required - toutes les propriétés requises
type ProduitRequis = Required<Produit>;
// Pick - sélectionner certaines propriétés
type ProduitResume = Pick<Produit, "id" | "nom" | "prix">;
// Omit - exclure certaines propriétés
type ProduitSansDescription = Omit<Produit, "description">;
// Record - créer un type avec des clés et valeurs spécifiques
type ProduitsParCategorie = Record<string, Produit[]>;
let produits: ProduitsParCategorie = {
"electronique": [
{ id: 1, nom: "Ordinateur", prix: 999, description: "PC portable", enStock: true }
],
"accessoires": [
{ id: 2, nom: "Souris", prix: 25, description: "Souris optique", enStock: true }
]
};
Types utilitaires personnalisés
// Type pour extraire les propriétés d'un certain type
type ProprietesDeType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
interface Exemple {
nom: string;
age: number;
actif: boolean;
score: number;
}
type ProprietesString = ProprietesDeType<Exemple, string>; // "nom"
type ProprietesNumber = ProprietesDeType<Exemple, number>; // "age" | "score"
// Type pour rendre certaines propriétés optionnelles
type RendreOptionnel<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type UtilisateurAvecEmailOptionnel = RendreOptionnel<Utilisateur, "email">;
// { id: number; nom: string; age?: number; email?: string; }
Génériques avec fonctions d’ordre supérieur
Map, Filter, Reduce génériques
class Collection<T> {
constructor(private elements: T[]) {}
map<U>(transformateur: (element: T) => U): Collection<U> {
return new Collection(this.elements.map(transformateur));
}
filter(predicat: (element: T) => boolean): Collection<T> {
return new Collection(this.elements.filter(predicat));
}
reduce<U>(
reducteur: (accumulateur: U, element: T) => U,
valeurInitiale: U
): U {
return this.elements.reduce(reducteur, valeurInitiale);
}
obtenirElements(): T[] {
return [...this.elements];
}
}
// Utilisation
let nombres = new Collection([1, 2, 3, 4, 5]);
let doubles = nombres
.filter(n => n % 2 === 0)
.map(n => n * 2);
console.log(doubles.obtenirElements()); // [4, 8]
let somme = nombres.reduce((acc, n) => acc + n, 0);
console.log(somme); // 15
Pipeline de transformation
interface Pipeline<T> {
valeur: T;
transformer<U>(transformateur: (valeur: T) => U): Pipeline<U>;
executer(): T;
}
class PipelineImpl<T> implements Pipeline<T> {
constructor(public valeur: T) {}
transformer<U>(transformateur: (valeur: T) => U): Pipeline<U> {
return new PipelineImpl(transformateur(this.valeur));
}
executer(): T {
return this.valeur;
}
}
function pipeline<T>(valeur: T): Pipeline<T> {
return new PipelineImpl(valeur);
}
// Utilisation
let resultat = pipeline(" hello world ")
.transformer(s => s.trim())
.transformer(s => s.toUpperCase())
.transformer(s => s.split(" "))
.transformer(arr => arr.join("-"))
.executer();
console.log(resultat); // "HELLO-WORLD"
Exemples pratiques
API Client générique
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface ApiError {
code: string;
message: string;
details?: any;
}
class ApiClient {
constructor(private baseUrl: string) {}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
return response.json();
}
async post<T, U>(endpoint: string, data: T): Promise<ApiResponse<U>> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
async put<T, U>(endpoint: string, data: T): Promise<ApiResponse<U>> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'DELETE'
});
return response.json();
}
}
// Utilisation
const client = new ApiClient("https://api.example.com");
interface Utilisateur {
id: number;
nom: string;
email: string;
}
interface CreerUtilisateurRequest {
nom: string;
email: string;
}
async function exempleUtilisation() {
// GET avec type de retour spécifié
const utilisateurs = await client.get<Utilisateur[]>("/utilisateurs");
// POST avec types de requête et réponse
const nouvelUtilisateur = await client.post<CreerUtilisateurRequest, Utilisateur>(
"/utilisateurs",
{ nom: "Alice", email: "alice@example.com" }
);
}
Cache générique
interface CacheEntry<T> {
valeur: T;
expiration: Date;
}
class Cache<K, V> {
private storage = new Map<K, CacheEntry<V>>();
private ttlMs: number;
constructor(ttlMs: number = 300000) { // 5 minutes par défaut
this.ttlMs = ttlMs;
}
set(cle: K, valeur: V): void {
const expiration = new Date(Date.now() + this.ttlMs);
this.storage.set(cle, { valeur, expiration });
}
get(cle: K): V | undefined {
const entry = this.storage.get(cle);
if (!entry) {
return undefined;
}
if (entry.expiration < new Date()) {
this.storage.delete(cle);
return undefined;
}
return entry.valeur;
}
has(cle: K): boolean {
return this.get(cle) !== undefined;
}
delete(cle: K): boolean {
return this.storage.delete(cle);
}
clear(): void {
this.storage.clear();
}
size(): number {
// Nettoyer les entrées expirées
const maintenant = new Date();
for (const [cle, entry] of this.storage.entries()) {
if (entry.expiration < maintenant) {
this.storage.delete(cle);
}
}
return this.storage.size;
}
}
// Utilisation
const cacheUtilisateurs = new Cache<number, Utilisateur>(600000); // 10 minutes
const cacheStrings = new Cache<string, string>();
cacheUtilisateurs.set(1, { id: 1, nom: "Alice", email: "alice@example.com" });
const utilisateur = cacheUtilisateurs.get(1);
Bonnes pratiques
- Utilisez des noms de paramètres génériques descriptifs (
T
pour Type,K
pour Key,V
pour Value) - Ajoutez des contraintes quand nécessaire pour la sécurité des types
- Préférez l’inférence de type quand possible
- Documentez vos génériques complexes
- Utilisez les types utilitaires intégrés avant de créer les vôtres
Conclusion
Les génériques sont un outil puissant pour créer du code réutilisable et type-safe. Ils permettent de :
- Éviter la duplication de code
- Maintenir la sécurité des types
- Créer des APIs flexibles
- Améliorer la réutilisabilité
Conseil : Commencez par des génériques simples et progressez vers des concepts plus avancés. Les génériques deviennent naturels avec la pratique.
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion