TypeScript types avances - 02 - Conditional types et infer

Comment écrire de la logique conditionnelle dans les types avec extends et extraire des sous-types avec infer. Le if/else du système de types.

02 - Conditional types et infer

Ce que tu vas apprendre

  • La syntaxe T extends U ? X : Y et 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 : Y est le ternaire des types — "si T est assignable a U, alors X, sinon Y"
  • infer capture un sous-type inconnu dans un pattern et le rend utilisable
  • ReturnType, Parameters, Awaited sont tous construits avec extends + infer
  • infer fonctionne 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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.