Aller au contenu

TypeScript : génériques et types utilitaires

Un générique est un type paramétré : on écrit une fois la logique et le type s’adapte à l’usage.

function premier<T>(liste: T[]): T | undefined {
return liste[0];
}
const n = premier([1, 2, 3]); // number | undefined
const s = premier(['a', 'b']); // string | undefined
const x = premier<boolean>([true]); // type précisé explicitement

T est déduit de l’argument : on précise rarement le type à la main. Un paramètre de type se nomme par convention T, U, ou un nom parlant (TCle, TValeur).

function associer<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
const paire = associer('Alice', 30); // [string, number]

Limiter les types acceptés :

function longueur<T extends { length: number }>(x: T): number {
return x.length;
}
longueur('abc'); // accepté
longueur([1, 2]); // accepté
longueur(42); // erreur : number n'a pas de « length »
function lire<T, K extends keyof T>(objet: T, cle: K): T[K] {
return objet[cle];
}
const client = { id: 1, nom: 'Alice' };
const nom = lire(client, 'nom'); // string
lire(client, 'age'); // erreur : « age » n'est pas une clé

T[K] est le type de la propriété K de T : le type de retour suit la clé choisie.

interface Resultat<T> {
donnees: T;
erreur?: string;
}
type Reponse<T> = { statut: number; corps: T };
const liste: Resultat<string[]> = { donnees: ['a'] };
interface Depot<T, TId = number> {
trouver(id: TId): Promise<T | undefined>;
enregistrer(entite: T): Promise<void>;
}

TId = number donne une valeur par défaut au paramètre de type.

class Pile<T> {
private elements: T[] = [];
empiler(element: T): void {
this.elements.push(element);
}
depiler(): T | undefined {
return this.elements.pop();
}
get taille(): number {
return this.elements.length;
}
}
const pile = new Pile<number>();
pile.empiler(1);
pile.empiler('a'); // erreur

Voir POO : polymorphisme et interfaces pour des dépôts génériques.

TypeScript fournit des types qui transforment d’autres types :

Type Effet Exemple
Partial<T> Toutes les propriétés deviennent facultatives Mise à jour partielle
Required<T> Toutes obligatoires
Readonly<T> Toutes en lecture seule
Pick<T, K> Garde certaines propriétés Pick<Client, 'id' | 'nom'>
Omit<T, K> Retire certaines propriétés Omit<Client, 'motDePasse'>
Record<K, V> Objet de clés K et valeurs V Record<string, number>
Exclude<U, V> Retire des membres d’une union
Extract<U, V> Garde des membres d’une union
NonNullable<T> Retire null et undefined
ReturnType<F> Type de retour d’une fonction
Parameters<F> Types des paramètres (tuple)
Awaited<T> Type résolu d’une promesse
interface Client {
id: number;
nom: string;
email: string;
motDePasse: string;
}
type ClientPublic = Omit<Client, 'motDePasse'>;
type MiseAJour = Partial<Omit<Client, 'id'>>;
type Resume = Pick<Client, 'id' | 'nom'>;
function modifier(client: Client, changements: MiseAJour): Client {
return { ...client, ...changements };
}
type Etat = 'ouvert' | 'ferme' | 'archive';
type Actifs = Exclude<Etat, 'archive'>; // 'ouvert' | 'ferme'
async function charger() {
return { id: 1, nom: 'Alice' };
}
type Chargement = Awaited<ReturnType<typeof charger>>; // { id: number; nom: string }

Dériver les types plutôt que les dupliquer évite qu’ils divergent : si Client change, ClientPublic suit.

type Drapeaux<T> = { [K in keyof T]: boolean };
type Optionnel<T> = { [K in keyof T]?: T[K] };
type SansLectureSeule<T> = { -readonly [K in keyof T]: T[K] };
interface Options {
couleur: string;
taille: number;
}
type ActivationOptions = Drapeaux<Options>; // { couleur: boolean; taille: boolean }

[K in keyof T] parcourt les clés de T. Les modificateurs ?, readonly, -?, -readonly ajoutent ou retirent des propriétés.

type EstTableau<T> = T extends unknown[] ? true : false;
type A = EstTableau<string[]>; // true
type B = EstTableau<number>; // false
type ElementDe<T> = T extends (infer U)[] ? U : never;
type E = ElementDe<number[]>; // number
type SansPromesse<T> = T extends Promise<infer U> ? U : T;

infer capture un type dans une condition. Les types conditionnels sont puissants mais difficiles à lire : s’en servir dans des bibliothèques utilitaires, pas dans chaque module.

type Methode = 'GET' | 'POST';
type Route = `/api/${string}`;
type Evenement = `${'clic' | 'survol'}:${'bouton' | 'lien'}`; // 'clic:bouton' | 'clic:lien' | 'survol:bouton' | 'survol:lien'
const route: Route = '/api/clients';
const mauvaise: Route = '/clients'; // erreur
const liste = new Map<string, number>();
const unique = new Set<string>();
const promesse: Promise<string[]> = Promise.resolve(['a']);
const tableau: Array<{ id: number }> = [];
async function recuperer<T>(url: string): Promise<T> {
const reponse = await fetch(url);
return (await reponse.json()) as T; // T est affirmé, pas vérifié
}
const clients = await recuperer<Client[]>('/api/clients');
  • Un générique doit servir : s’il n’apparaît qu’une fois dans la signature, un type simple suffit.
  • Nommer les paramètres de type quand il y en a plusieurs (TEntite, TId).
  • Préférer les types utilitaires prédéfinis aux types mappés écrits à la main.
  • Éviter les types conditionnels profonds : ils ralentissent le compilateur et dégradent les messages d’erreur.
Symptôme Piste
Type 'X' does not satisfy the constraint 'Y' Argument de type incompatible avec extends
Type 'T' is not assignable to type 'U' Le compilateur ne peut pas prouver la conversion : contrainte, affinage ou surcharge
Property 'x' does not exist on type 'T' Ajouter une contrainte (T extends { x: … })
Generic type 'X' requires N type argument(s) Argument de type manquant
Type instantiation is excessively deep and possibly infinite Type récursif ou conditionnel trop profond : simplifier
Message d’erreur illisible Décomposer les types en alias nommés

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune.