Maîtrisez la distributivité des types conditionnels, le filtrage de motif avec infer et les cas limites comme never, any et boolean.
Ouvrir cette leçon dans KodokonLes 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.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;
// true
type B = IsString<string | number>;
// boolean, et non false !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.
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 tupleLe 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.
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 }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.
type ToNumber<S extends string> =
S extends `${infer N extends number}` ? N : never;
type Port = ToNumber<"8080">; // le litteral 8080
type Bad = ToNumber<"abc">; // neverExclude<boolean, true> ?type IsNever<T> = T extends never ? true : false;, que vaut IsNever<never> ?infer P du même nom placés en position de paramètre de fonction produisent :