0%
Génériques en TypeScript

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

  1. Utilisez des noms de paramètres génériques descriptifs (T pour Type, K pour Key, V pour Value)
  2. Ajoutez des contraintes quand nécessaire pour la sécurité des types
  3. Préférez l’inférence de type quand possible
  4. Documentez vos génériques complexes
  5. 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

Lien copié !