frontend / typescript
Conditional types et infer
Explication
Ce que vous allez apprendre
- Écrire un type conditionnel avec la syntaxe
T extends U ? A : B - Capturer un fragment d'un type avec
inferà l'intérieur d'une condition - Reconstruire un utilitaire natif comme
ReturnTypeouElementDeavecinfer - Identifier la distribution automatique d'un type conditionnel sur une union
- Empêcher cette distribution en enveloppant le type dans un tuple à un élément
Dans quel contexte ?
Une équipe travaille sur une fonction générique premierElement<T>(tableau: T) censée renvoyer le type d'un seul élément du tableau, mais découvre que EstTableau<string | number[]> produit un résultat surprenant : au lieu d'évaluer l'union entière, TypeScript applique automatiquement le type conditionnel à chaque membre de l'union séparément (distribution), donnant un résultat combiné inattendu. Comprendre ce mécanisme de distribution, et savoir l'empêcher en enveloppant le type dans un tuple [T] extends [U] ? A : B, est nécessaire pour écrire des utilitaires de type fiables sur des unions.
Raisonner sur les types comme sur des valeurs
Jusqu'ici, vos types ont toujours été fixes et déclarés explicitement. Les types conditionnels changent cette logique : ils permettent d'écrire une condition qui s'évalue au niveau des types eux-mêmes, avec une syntaxe qui ressemble à un opérateur ternaire (T extends U ? A : B) mais qui ne s'exécute jamais au runtime — tout se passe pendant la compilation.
Prérequis
Cette leçon a un niveau clairement avancé : elle suppose les génériques (leçon 5) et les utility types (leçon 8) bien maîtrisés, puisqu'elle explique comment ces derniers sont construits en interne.
infer : capturer une partie d'un type
Le mot-clé infer permet, à l'intérieur d'une condition, de capturer un fragment d'une structure de type pour le réutiliser dans la branche vraie. C'est ainsi que sont construits, en interne, des utilitaires que vous connaissez déjà comme ReturnType, qui capture ce qu'une fonction retourne, ou Awaited, qui capture la valeur enveloppée dans une Promise. Comprendre infer permet de construire ses propres utilitaires quand ceux fournis nativement ne suffisent pas.
| Type conditionnel | Rôle |
|---|---|
T extends U ? A : B | Choisir un type selon une condition évaluée à la compilation |
T extends (infer E)[] ? E : never | Capturer le type d'élément d'un tableau |
T extends (...args: any[]) => infer R ? R : never | Capturer le type de retour d'une fonction |
Un piège à connaître : la distribution automatique
Quand un type conditionnel est appliqué à une union nue, directement passée en paramètre générique, TypeScript l'applique automatiquement à chaque membre de l'union séparément, plutôt qu'à l'union entière d'un coup. Ce comportement, appelé distribution, est souvent ce qu'on souhaite, mais pas toujours. Pour l'empêcher, il suffit d'envelopper le type dans un tuple à un seul élément, une astuce syntaxique à connaître plutôt qu'à deviner.
Piège fréquent
type SansNull<T> = T extends null ? never : T; appliqué à SansNull<string | null> distribue automatiquement sur chaque membre de l'union, ce qui donne ici le résultat voulu (string). Mais si ce comportement n'est pas désiré, enveloppez les deux côtés dans un tuple : [T] extends [null] ? never : T.
Un outil pour du code générique avancé
Ces types conditionnels deviennent particulièrement puissants une fois combinés aux mapped types et aux template literal types, vus dans les deux prochaines leçons — ensemble, ils permettent de construire des types extrêmement précis à partir de peu de code.
Commandes & code
Conditional types et infer
// Type conditionnel : "si T est assignable à U, alors A, sinon B"
type EstString<T> = T extends string ? true : false;
type A = EstString<"salut">; // true
type B = EstString<42>; // false
// infer : capture un sous-type à l'intérieur d'une structure
type ElementDe<T> = T extends (infer E)[] ? E : never;
type Nombre = ElementDe<number[]>; // number
type Jamais = ElementDe<string>; // never (string n'est pas un tableau)
// infer sur une fonction : extraire le type de retour (comme ReturnType natif)
type MonReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function creerId(): string {
return crypto.randomUUID();
}
type IdRetourne = MonReturnType<typeof creerId>; // string
// infer sur une Promise : extraire la valeur résolue (comme Awaited natif)
type Deballer<T> = T extends Promise<infer V> ? V : T;
type Valeur = Deballer<Promise<number>>; // number
// Types conditionnels distributifs : appliqués membre par membre sur une union
type VersTableau<T> = T extends any ? T[] : never;
type Resultat = VersTableau<string | number>; // string[] | number[] (PAS (string|number)[])
// Empêcher la distribution en enveloppant T dans un tuple à un élément
type VersTableauNonDistribue<T> = [T] extends [any] ? T[] : never;
type ResultatNonDistribue = VersTableauNonDistribue<string | number>; // (string | number)[]
// Cas pratique : typer une fonction "safe" qui ne throw jamais
type Resultat_<T, E = Error> = { succes: true; valeur: T } | { succes: false; erreur: E };
function essayer<T>(fn: () => T): Resultat_<T> {
try {
return { succes: true, valeur: fn() };
} catch (e) {
return { succes: false, erreur: e as Error };
}
}
// Type récursif conditionnel : aplatit un type de tableau imbriqué
type Aplatir<T> = T extends (infer U)[] ? Aplatir<U> : T;
type Profond = Aplatir<number[][][]>; // numberRésumé
T extends U ? A : Bévalue une condition au niveau des TYPES, pas des valeurs.infercapture une partie d'un type pour la réutiliser dans la branche "vrai".- Un conditionnel appliqué à une union nue se distribue automatiquement membre par membre.
Exercices pratiques
Mission : dompter la distribution automatique d'un type conditionnel
Objectif : Diagnostiquer un résultat surprenant produit par la distribution automatique d'un type conditionnel, puis l'empêcher volontairement.
Contexte
Une équipe écrit type EstTableau<T> = T extends any[] ? true : false; et découvre que EstTableau<string | number[]> ne donne pas un simple booléen mais false | true. Ta mission : comprendre le mécanisme de distribution en jeu, puis apprendre à l'empêcher quand ce n'est pas le résultat voulu.