Kodokon kodokon.com

Types conditionnels et infer : la logique du compilateur

Maîtrisez la distributivité des types conditionnels, le filtrage de motif avec infer et les cas limites comme never, any et boolean.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

Les types conditionnels sont le mécanisme de branchement du système de types : T extends U ? X : Y demande au compilateur si T est assignable à U, puis sélectionne une branche. Tous les utilitaires standards comme ReturnType, Exclude ou Awaited reposent sur eux. À ce niveau, la question n'est plus de savoir les écrire, mais de prédire exactement comment le compilateur les évalue, notamment face aux unions et à never.

TYPESCRIPT
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;
// true

type B = IsString<string | number>;
// boolean, et non false !
Un résultat surprenant sur une union : boolean.

Le comportement clé est la distributivité : quand le type testé est un paramètre de type nu (non enveloppé dans un tuple, un tableau ou une intersection), le conditionnel est appliqué séparément à chaque membre de l'union, puis les résultats sont réunis. IsString<string | number> calcule donc true | false, c'est-à-dire boolean. Pour désactiver cette distribution, enveloppez les deux côtés dans un tuple : [T] extends [U]. Cas limite redoutable : never est l'union vide ; distribuer sur zéro membre n'évalue aucune branche et le résultat est never, quelle que soit la branche écrite.

TYPESCRIPT
type ToArray<T> = T extends unknown ? T[] : never;
type NoDist<T> = [T] extends [unknown] ? T[] : never;

type A = ToArray<string | number>;
// string[] | number[]

type B = NoDist<string | number>;
// (string | number)[]

type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true grace au tuple
Le tuple neutralise la distribution et fiabilise never.

Le mot-clé infer introduit une variable de type capturée par filtrage de motif : le compilateur tente de faire correspondre la structure du type testé au motif, et lie la variable si la correspondance réussit. Vous pouvez placer plusieurs infer du même nom : en position covariante (valeurs produites, comme un type de retour), les candidats sont réunis en union ; en position contravariante (paramètres de fonction), ils sont combinés en intersection. Cette règle découle directement de la variance : c'est le seul choix sûr dans chaque direction.

TYPESCRIPT
type ElementOf<T> =
  T extends readonly (infer E)[] ? E : never;

type FirstParam<T> =
  T extends (arg: infer P) => unknown ? P : never;

type Merged<T> =
  T extends { a: (x: infer P) => void;
              b: (x: infer P) => void }
    ? P : never;

type M = Merged<{
  a: (x: { id: string }) => void;
  b: (x: { tag: string }) => void;
}>; // { id: string } & { tag: string }
Deux infer en position contravariante : intersection.

Depuis TypeScript 4.7, infer accepte une contrainte directe : infer N extends number filtre la capture, et depuis la 4.8 le compilateur convertit même un littéral de chaîne numérique en littéral de nombre. Autre subtilité de spécification : un type conditionnel dont l'entrée contient encore un paramètre générique non résolu reste différé ; il ne s'évalue qu'au site d'instanciation, ce qui explique pourquoi certaines erreurs n'apparaissent que chez l'appelant.

TYPESCRIPT
type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type Port = ToNumber<"8080">; // le litteral 8080
type Bad = ToNumber<"abc">;   // never
Contrainte sur infer et conversion de littéral (4.8).

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Que vaut le type Exclude<boolean, true> ?
    • boolean
    • false
    • never
    • true
  2. Avec type IsNever<T> = T extends never ? true : false;, que vaut IsNever<never> ?
    • true
    • false
    • never
    • boolean
  3. Deux infer P du même nom placés en position de paramètre de fonction produisent :
    • une union des candidats
    • une intersection des candidats
    • le premier candidat rencontré
    • une erreur de compilation