Menu

Types conditionnels et infer en TypeScript, avec exemples

Un type conditionnel choisit l'un de deux types selon un test : T extends U ? X : Y. Découvrez la syntaxe, comment les types conditionnels se distribuent sur les unions (et comment l'empêcher), l'extraction de types avec infer, et comment écrire ReturnType vous-même.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Un type conditionnel choisit entre deux types grâce à un test qui se lit comme le ternaire de JavaScript : T extends U ? X : Y. Si T est assignable à U, le résultat est X ; sinon, c'est Y.

Ici, extends signifie « est assignable à », la même relation que le compilateur utilise quand vous assignez une valeur à une variable. Les types conditionnels n'existent qu'à la compilation ; ils sont effacés de la sortie JavaScript.

La syntaxe

type Result = CheckedType extends TestType ? TrueType : FalseType;

Les types conditionnels deviennent utiles avec les génériques, où le type vérifié est un paramètre de type qui reçoit plus tard un type concret. Dans la branche true, TypeScript sait que le type vérifié correspond au test, c'est pourquoi T["message"] ci-dessus est autorisé alors qu'un simple T n'a pas de propriété message.

Ils s'imbriquent aussi, comme des ternaires enchaînés :

Le as TypeName<T> dans le corps de la fonction n'est pas facultatif, comme le montre la section suivante.

Les types de retour conditionnels demandent une assertion

Une fonction dont le type de retour est un type conditionnel sur son propre paramètre de type ne peut renvoyer directement aucune des deux branches. TypeScript n'affine pas T dans le corps, il ne peut donc pas savoir quelle branche s'applique :

index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.

Deux corrections courantes : des surcharges, qui énoncent chaque paire entrée/sortie et vérifient précisément les appelants, ou une assertion dans l'implémentation.

Les surcharges sont présentées sur la page de surcharge de fonctions. Avec une assertion, le compilateur vous fait confiance : une mauvaise branche dans le corps ne serait pas détectée.

Types conditionnels distributifs

Quand le type vérifié est un paramètre de type nu et qu'il reçoit une union, la condition s'exécute une fois par membre et les résultats sont réunis en une nouvelle union :

C'est la distribution qui fait fonctionner Exclude et Extract. Exclude<T, U> est défini comme T extends U ? never : T : chaque membre qui correspond à U devient never, et never disparaît d'une union. Ainsi, Exclude<"a" | "b" | "c", "a"> vaut "b" | "c".

Deux surprises découlent de la même règle. boolean est l'union true | false, donc ToArray<boolean> vaut false[] | true[], et non boolean[]. Et never est l'union vide : un type conditionnel distributif qui reçoit never renvoie donc never sans rien tester :

type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true

type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true

Extraire des types avec infer

infer déclare une nouvelle variable de type dans la clause extends. Si la correspondance réussit, TypeScript remplit cette variable à partir du type vérifié, et vous pouvez l'utiliser dans la branche true :

Lisez T extends Promise<infer V> ? V : T ainsi : « si T est une promesse de quelque chose, appelle ce quelque chose V et renvoie-le ; sinon, renvoie T inchangé ». infer n'est autorisé que dans la clause extends d'un type conditionnel.

Une variable infer peut porter sa propre contrainte avec extends. La correspondance ne réussit alors que si le type inféré la respecte :

type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;

type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string

Écrire ReturnType vous-même

Le ReturnType intégré est un type conditionnel d'une ligne avec infer. L'écrire vous-même est l'exercice classique qui fait comprendre les deux idées :

typeof makeUser transforme la valeur fonction en son type, puis le type conditionnel la compare à « n'importe quelle fonction » et capture le type de retour dans R. La version de la bibliothèque standard diffère sur deux détails : son paramètre est contraint aux types de fonctions (T extends (...args: any) => any), donc ReturnType<string> est une erreur de compilation plutôt que never, et sa branche fausse vaut any. La page ReturnType présente aussi Parameters, InstanceType et Awaited.

Types conditionnels récursifs

Un type conditionnel peut faire référence à lui-même, ce qui lui permet de déballer n'importe quelle profondeur d'imbrication :

type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;

type A = Flatten<number[][][]>; // number
type B = Flatten<string>;       // string

Le type intégré Awaited<T> fonctionne ainsi, en déballant Promise<Promise<T>> jusqu'à T. En pratique, gardez la récursion peu profonde : une récursion très profonde ou illimitée fait abandonner le compilateur avec error TS2589: Type instantiation is excessively deep and possibly infinite.

Référence rapide

MotifSignification
T extends U ? X : YX si T est assignable à U, sinon Y
T extends U ? never : Tretirer les membres qui correspondent à U (c'est Exclude)
T extends U ? T : nevergarder les membres qui correspondent à U (c'est Extract)
[T] extends [U] ? X : Ymême test, sans distribution sur une union
T extends (infer E)[] ? E : Ttype des éléments d'un tableau
T extends Promise<infer V> ? V : Ttype de la valeur d'une promesse
T extends (...args: any[]) => infer R ? R : nevertype de retour d'une fonction
T extends [infer H, ...infer Rest] ? ...premier élément et reste d'un tuple

Questions fréquentes

Qu'est-ce qu'un type conditionnel en TypeScript ?

Un type de la forme T extends U ? X : Y. Si T est assignable à U, le résultat est X, sinon Y. C'est un if/else pour les types, évalué à la compilation ; rien n'en subsiste dans le JavaScript émis.

Que fait le mot-clé infer en TypeScript ?

infer déclare une variable de type dans la clause extends d'un type conditionnel et laisse TypeScript la remplir à partir du type qui correspond. T extends Promise<infer V> ? V : T extrait le type de la valeur résolue d'un type promesse. Il ne peut être utilisé que dans la clause extends d'un type conditionnel.

Qu'est-ce qu'un type conditionnel distributif ?

Quand le type vérifié est un paramètre de type nu et que vous passez une union, la condition est appliquée à chaque membre séparément et les résultats sont réunis. ToArray<string | number> avec type ToArray<T> = T extends unknown ? T[] : never devient string[] | number[]. Entourez les deux côtés de crochets, [T] extends [unknown], pour désactiver ce comportement.

Comment obtenir le type de retour d'une fonction en TypeScript ?

Utilisez le type intégré ReturnType<typeof fn>. C'est un type conditionnel avec infer : T extends (...args: any) => infer R ? R : any. Pour les fonctions async, enveloppez-le dans Awaited<...> pour obtenir la valeur résolue plutôt que la promesse.

Pourquoi IsNever<never> renvoie-t-il never au lieu de true ?

never est l'union vide, et un type conditionnel distributif s'applique aux membres d'une union. Sans aucun membre, il n'y a rien à parcourir et le résultat est never. Écrivez [T] extends [never] ? true : false pour tester never lui-même.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER