02 - Conditional types et infer
Ce que tu vas apprendre
- La syntaxe
T extends U ? X : Yet comment elle fonctionne - Comment extraire un sous-type avec
infer - Reconstruire ReturnType, Parameters, Awaited depuis zero
- Les patterns courants et les combinaisons avec mapped types
Prerequisites
Avoir lu l'article sur les mapped types.
Le if/else des types
En JavaScript, tu ecris :
typescriptconst result = condition ? valueA : valueB
En TypeScript, au niveau des types :
typescripttype Result = T extends string ? "text" : "other"
extends dans un conditional type ne veut pas dire "hérité de". Ca veut dire "est assignable a". Si T est assignable a string, le type est "text". Sinon, c'est "other".
typescripttype IsString<T> = T extends string ? true : false
type A = IsString<"hello"> // true
type B = IsString<42> // false
type C = IsString<string> // true
C'est un ternaire au niveau des types. Le résultat n'est pas une valeur, c'est un type.
Enchainer les conditions
Tu peux imbriquer les conditional types comme des if/else if/else :
typescripttype TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends Function ? "function" :
T extends undefined ? "undefined" :
"object"
type A = TypeName<string> // "string"
type B = TypeName<() => void> // "function"
type C = TypeName<Date> // "object"
infer : extraire un type inconnu
infer est le mecanisme le plus puissant des conditional types. Il déclaré une variable de type a l'intérieur d'une condition extends et capture le type correspondant.
typescripttype GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type A = GetReturnType<() => string> // string
type B = GetReturnType<(x: number) => boolean> // boolean
type C = GetReturnType<string> // never — string n'est pas une fonction
infer R dit : "si T correspond au pattern d'une fonction, capture le type de retour dans R". C'est exactement ce que fait ReturnType<T> de TypeScript.
Reconstruire les utility types
Avec infer, tu peux reconstruire tous les utility types d'extraction :
typescript// ReturnType : extraire le retour d'une fonction
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any
// Parameters : extraire les parametres d'une fonction
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never
// Awaited : deballer une Promise
type Awaited<T> =
T extends Promise<infer U> ? Awaited<U> : T
// ConstructorParameters : extraire les params d'un constructeur
type ConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never
Awaited est recursif : si le résultat est encore une Promise, il continue a deballer. Awaited<Promise<Promise<string>>> donne string.
infer dans les tableaux et tuples
typescript// Premier element d'un tuple
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never
// Tout sauf le premier
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : never
// Dernier element
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never
type H = Head<[string, number, boolean]> // string
type T = Tail<[string, number, boolean]> // [number, boolean]
type L = Last<[string, number, boolean]> // boolean
infer dans les strings
Depuis TypeScript 4.1, infer fonctionne avec les template literal types :
typescripttype ExtractId<T> = T extends `user_${infer Id}` ? Id : never
type A = ExtractId<"user_123"> // "123"
type B = ExtractId<"order_456"> // never
// Extraire les parties d'une route
type ParseRoute<T> = T extends `/${infer Segment}/${infer Rest}`
? [Segment, ...ParseRoute<`/${Rest}`>]
: T extends `/${infer Segment}`
? [Segment]
: []
type R = ParseRoute<"/api/users/123"> // ["api", "users", "123"]
L'article sur les template literal types approfondira ces patterns.
Patterns concrets
Extraire le type d'une propriété spécifique
typescripttype PropType<T, K extends string> =
T extends Record<K, infer V> ? V : never
type UserName = PropType<{ name: string; age: number }, "name"> // string
Aplatir un type
typescripttype Flatten<T> = T extends Array<infer U> ? U : T
type A = Flatten<string[]> // string
type B = Flatten<number> // number
type C = Flatten<(string | number)[]> // string | number
Type-safe event emitter
Sur paltemps.fr, j'utilise un pattern pour typer les callbacks d'un event emitter. Le conditional type extrait le type de callback attendu pour chaque événement :
typescriptinterface EventMap {
"user:login": { userId: string }
"user:logout": { userId: string; reason: string }
"error": { code: number; message: string }
}
type CallbackFor<E extends keyof EventMap> = (data: EventMap[E]) => void
function on<E extends keyof EventMap>(event: E, callback: CallbackFor<E>) {
// ...
}
on("user:login", (data) => {
console.log(data.userId) // ✅ type: string
})
Unwrap imbriques
typescripttype UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T
type UnwrapArray<T> = T extends Array<infer U> ? UnwrapArray<U> : T
type A = UnwrapPromise<Promise<Promise<string>>> // string
type B = UnwrapArray<number[][][]> // number
Combiner conditional types et mapped types
Le vrai pouvoir apparaît quand tu combines les deux :
typescript// Rendre optionnelles uniquement les proprietes qui sont des objets
type PartialObjects<T> = {
[K in keyof T]: T[K] extends object ? T[K] | undefined : T[K]
}
// Extraire uniquement les methodes d'un objet
type MethodsOf<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K]
}
interface UserService {
name: string
age: number
greet(): string
save(): Promise<void>
}
type Methods = MethodsOf<UserService>
// { greet: () => string; save: () => Promise<void> }
Le mapped type itéré, le conditional type filtre. Ensemble, ils permettent de transformer des types avec une precision chirurgicale.
Résumé
T extends U ? X : Yest le ternaire des types — "si T est assignable a U, alors X, sinon Y"infercapture un sous-type inconnu dans un pattern et le rend utilisable- ReturnType, Parameters, Awaited sont tous construits avec
extends+infer inferfonctionne dans les fonctions, tableaux, tuples, et template literal strings- Combine conditional types + mapped types pour des transformations de types chirurgicales
Article précédent : 01 - Mapped types
Article suivant : 03 - Distributive conditional types