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.
tsTryinterfaceAnimal {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 :
tsTrySomeType 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 :
tsTryinterfaceIdLabel {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 quecreateLabelpeut gérer, le nombre de surcharges croît exponentiellement.
À la place, nous pouvons décrire cette logique avec un type conditionnel :
tsTrytypeNameOrId <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.
tsTryfunctioncreateLabel <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 :
tsTrytypeType '"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 :
tsTrytypeMessageOf <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 :
tsTrytypeMessageOf <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 :
tsTrytypeFlatten <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” :
tsTrytypeFlatten <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 :
tsTrytypeGetReturnType <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.
tsTrydeclare 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 :
tsTrytypeToArray <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.
tsTrytypeToArray <Type > =Type extends any ?Type [] : never;typeStrArrOrNumArr =ToArray <string | number>;
StrArrOrNumArr se distribue sur :
tsTrystring | number;
et s’applique à chaque membre de l’union, ce qui donne :
tsTryToArray <string> |ToArray <number>;
En suivant la logique de ToArray avec string et number, on finit par obtenir :
tsTrystring[] | 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.
tsTrytypeToArrayNonDist <Type > = [Type ] extends [any] ?Type [] : never;// 'StrArrOrNumArr' is no longer a union.typeStrArrOrNumArr =ToArrayNonDist <string | number>;