Les programmes un tant soit utiles doivent se baser sur une entrée utilisateur. Ce n’est pas plus différent en JavaScript, mais comme les valeurs peuvent être facilement examinées, ces décisions se basent également sur le type de ces valeurs. Les types conditionnels décrivent les relations entre les types en entrée et en sortie.
tsTry
interfaceAnimal {live (): void;}interfaceDog extendsAnimal {woof (): void;}typeExample1 =Dog extendsAnimal ? number : string;typeExample2 =RegExp extendsAnimal ? number : string;
Les types conditionnels prennent une forme similaire aux expressions ternaires (condition ? trueExpression : falseExpression
) en JavaScript :
tsTry
SomeType extendsOtherType ?TrueType :FalseType ;
Quand le type à gauche d’extends
peut être assigné au type de droite, le résultat sera le type dans la première branche (la branche “vrai”); sinon ce sera le type dans la deuxième branche (la branche “false”).
Ces exemples ne montrent pas forcément l’intérêt des conditions, vu qu’on peut voir si Dog extends Animal
et décider entre number
et string
de nous-même.
Cet intérêt se manifeste surtout en utilisant les types génériques.
Considérons cette fonction createLabel
:
tsTry
interfaceIdLabel {id : number /* + d'autres champs */;}interfaceNameLabel {name : string /* + d'autres champs */;}functioncreateLabel (id : number):IdLabel ;functioncreateLabel (name : string):NameLabel ;functioncreateLabel (nameOrId : string | number):IdLabel |NameLabel ;functioncreateLabel (nameOrId : string | number):IdLabel |NameLabel {throw "unimplemented";}
Ces surcharges de createLabel décrivent une seule fonction JavaScript qui fait des choix en fonction du type de son entrée. Notez, cependant, quelques problèmes :
- Si une librairie doit faire à chaque fois plusieurs choix à travers son API, toutes ces surcharges peuvent vite polluer le code.
- Trois surcharges doivent être créées : une pour chaque cas où vous êtes sûrs et certains du type de votre valeur (un cas pour
string
, un pournumber
), et une surcharge plus générale (string | number
). Pour chaque nouveau type quecreateLabel
peut gérer, le nombre de surcharges croît exponentiellement.
À la place, nous pouvons décrire cette logique avec un type conditionnel :
tsTry
typeNameOrId <T extends number | string> =T extends number?IdLabel :NameLabel ;
Nous pouvons ensuite utiliser les types conditionnels pour éliminer les surcharges et simplifier la signature de la fonction.
tsTry
functioncreateLabel <T extends number | string>(idOrName :T ):NameOrId <T > {throw "unimplemented";}leta =createLabel ("typescript");letb =createLabel (2.8);letc =createLabel (Math .random () ? "bonjour" : 42);
Contraintes de Types Conditionnels
Les vérifications sur des types conditionnels vont souvent révéler de nouvelles informations. Tout comme rétrécir avec des gardes de types peut donner un type plus spécifique, la branche “vrai” du type conditionnel va restreindre le type générique qu’on vérifie avec la contrainte demandée.
Prenons cet exemple :
tsTry
typeType '"message"' cannot be used to index type 'T'.2536Type '"message"' cannot be used to index type 'T'.MessageOf <T > =T ["message"];
TypeScript signale une erreur parce que T
n’aura pas forcément une propriété message
.
Il serait possible de contraindre T
, et TypeScript ne donnera plus d’erreur :
tsTry
typeMessageOf <T extends {message : unknown }> =T ["message"];interfacemessage : string;}typeEmailMessageContents =MessageOf <
Mais si on voulait que MessageOf
prenne tout, mais soit égal à never
s’il n’y a pas de propriété message
?
Nous pouvons déplacer la contrainte et introduire un type conditionnel :
tsTry
typeMessageOf <T > =T extends {message : unknown } ?T ["message"] : never;interfacemessage : string;}interfaceDog {bark (): void;}typeEmailMessageContents =MessageOf <typeDogMessageContents =MessageOf <Dog >;
Dans la branche “vrai”, TypeScript sait que T
va avoir une propriété message
.
Dans un tout autre exemple, nous pouvons aussi écrire un type Flatten
qui aplatit les tableaux en récupérant les types de leurs contenus, mais laisse les types tels quels sinon :
tsTry
typeFlatten <T > =T extends any[] ?T [number] :T ;// Extraction du type des éléments de tableautypeStr =Flatten <string[]>;// Laisse le type tranquille.typeNum =Flatten <number>;
Quand Flatten
reçoit un type tableau, il utilise un accès indexé avec number
pour récupérer le type des éléments de string[]
.
Sinon, il retourne simplement le type qui lui a été donné.
Inférence dans les Types Conditionnels
Nous avons utilisé des types conditionnels pour appliquer des contraintes et extraire des types. Cette opération devient très facile avec ces types, qu’elle est devenue très commune.
Les types conditionnels fournissent une façon d’inférer depuis les types qu’on compare avec le mot-clé infer
.
Par exemple, on pouvait inférer le type d’éléments de tableaux dans Flatten
au lieu de le récupérer “manuellement” :
tsTry
typeFlatten <Type > =Type extendsArray <inferItem > ?Item :Type ;
Ici, le mot-clé infer
introduit un nouveau type générique variable appelé Item
, au lieu de préciser comment récupérer le type élément de T
dans la branche vrai.
Cela nous libère de devoir penser à la façon de creuser et obtenir manuellement les types qui nous intéressent.
Nous pouvons écrire des types utiles grâce au mot-clé infer
.
Pour les cas simples, il est possible d’inférer le type de retour d’une fonction :
tsTry
typeGetReturnType <Type > =Type extends (...args : never[]) => inferReturn ?Return : never;typeNum =GetReturnType <() => number>;typeStr =GetReturnType <(x : string) => string>;typeBools =GetReturnType <(a : boolean,b : boolean) => boolean[]>;
Lorsqu’on infère depuis une fonction qui a plusieurs signatures, le résultat est l’inférence de la dernière (et donc probablement la plus laxiste). Il n’est pas possible d’inférer une signature particulière en se basant sur une liste d’arguments de types.
tsTry
declare functionstringOrNum (x : string): number;declare functionstringOrNum (x : number): string;declare functionstringOrNum (x : string | number): string | number;typeT1 =ReturnType <typeofstringOrNum >;
Types Conditionnels Distributifs
Quand les types conditionnels opèrent sur un type générique, ils deviennent distributifs quand on utilise également un type union. Par exemple :
tsTry
typeToArray <Type > =Type extends any ?Type [] : never;
Si ToArray
reçoit un type union, le type conditionnel va s’appliquer sur chaque membre de ce type union.
tsTry
typeToArray <Type > =Type extends any ?Type [] : never;typeStrArrOrNumArr =ToArray <string | number>;
StrArrOrNumArr
se distribue sur :
tsTry
string | number;
et s’applique à chaque membre de l’union, ce qui donne :
tsTry
ToArray <string> |ToArray <number>;
En suivant la logique de ToArray
avec string
et number
, on finit par obtenir :
tsTry
string[] | number[];
La distributivité est le comportement voulu d’habitude.
Pour éviter ce comportement, vous pouvez entourer chaque côté du mot-clé extends
avec des crochets.
tsTry
typeToArrayNonDist <Type > = [Type ] extends [any] ?Type [] : never;// 'StrArrOrNumArr' is no longer a union.typeStrArrOrNumArr =ToArrayNonDist <string | number>;