Imaginez que vous avez une fonction padLeft.
tsTryfunctionpadLeft (padding : number | string,input : string): string {throw newError ("Pas encore implémenté !");}
Si padding est un number, la fonction va le traiter comme le nombre d’espaces à insérer avant input.
Si padding est un string, elle doit juste concaténer padding avant input.
Essayons d’implémenter la logique d’implémentation de padLeft quand elle reçoit un number pour padding.
tsTryfunctionpadLeft (padding : number | string,input : string) {return " ".Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.2345Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.repeat () + padding input ;}
Nous avons une erreur au niveau de padding.
TypeScript nous avertit qu’assigner un number | string à un number pourrait ne pas nous donner ce qu’on veut, et c’est vrai.
En d’autres termes, nous n’avons pas vérifié explicitement si padding est d’abord un number, et nous ne gérons pas non plus les cas où on envoie un string, alors faisons ça.
tsTryfunctionpadLeft (padding : number | string,input : string) {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
Si cela ressemble à du code JavaScript inintéressant, c’est l’objectif. À part les annotations que nous avons mises en places, ce code TypeScript ressemble à du JavaScript. L’idée est que le but du système de types de TypeScript est de pouvoir écrire du code JavaScript de tous les jours sans se plier en quatre pour garantir la sécurité de typage.
Et même si ce code n’a pas l’air impressionnant, il y a beaucoup de choses qui se passent en coulisses.
TypeScript peut très bien analyser les valeurs à l’exécution en utilisant des types statiques. Il peut aussi suivre les expressions de contrôle de flux de code de JavaScript comme les if/else, les conditions ternaires, boucles, vérifications de véracité, etc., qui peuvent affecter ces types.
À l’intérieur de cet if, TypeScript repère typeof padding === "number" et comprend que c’est du code spécial qui s’appelle les gardes de types.
TypeScript suit les possibles chemins d’exécution que le programme peut prendre pour analyser les types les plus spécifiques possibles d’une valeur à tout moment.
Le procédé de raffinement de types vers des types plus spécifiques s’appelle le rétrécissement.
Dans plusieurs éditeurs de code, ainsi que dans les exemples suivants, il est possible d’observer ces types dès qu’ils changent.
tsTryfunctionpadLeft (padding : number | string,input : string) {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
Il y a un nombre d’expressions que TypeScript peut comprendre, pour permettre de faire du rétrécissement.
Gardes de types typeof
Comme vu précédemment, le JavaScript supporte un opérateur typeof qui donne des informations très basiques sur le type de la valeur vérifiée.
TypeScript s’attend à ce que cet opérateur retourne un certain ensemble précis de valeurs:
"string""number""bigint""boolean""symbol""undefined""object""function"
Comme vu avec padLeft, cet opérateur apparaît souvent dans beaucoup de librairies JavaScript, et TypeScript peut s’en servir pour opérer son rétrécissement dans les branches de code.
En TypeScript, le fait de vérifier le type d’une valeur avec typeof crée un garde de type.
TypeScript sait comment typeof agit avec différentes valeurs, y compris avec les excentricités de JavaScript.
Par exemple, dans la liste précédente, vous remarquerez que typeof ne retourne pas null.
Vous le verrez aussi dans cet exemple :
tsTryfunctionprintAll (strs : string | string[] | null) {if (typeofstrs === "object") {for (const'strs' is possibly 'null'.18047'strs' is possibly 'null'.s of) { strs console .log (s );}} else if (typeofstrs === "string") {console .log (strs );} else {// do nothing}}
La fonction printAll essaie de vérifier si strs est un objet, pour itérer sur le tableau (rappel : les tableaux sont des objets en JavaScript).
Le problème est que typeof null est également "object", ouvrant la porte à une faille qui permet de provoquer un crash.
C’est l’un des malheureux accidents à jamais présents dans l’histoire.
Les développeurs suffisamment expérimentés ne seront peut-être pas surpris, mais pas tout le monde a croisé ce cas de figure ; heureusement, TypeScript vous fera savoir que strs n’a été rétréci que vers string[] | null, au lieu de string[] uniquement.
Ce serait une bonne transition vers ce qu’on va nommer, le rétrécissement par véracité.
Rétrécissement par véracité
La véracité (anglais truthiness) ne va pas être un mot que vous allez souvent croiser, mais c’est quelque chose de très commun en JavaScript.
Vous pouvez utiliser des expressions (sans qu’elles retournent nécessairement un booléen), des &&, des ||, des if, des négations booléennes (!), et plus encore.
Par exemple, les if ne s’attendent pas spécialement à ce que l’expression retourne un boolean.
tsTryfunctiongetUsersOnlineMessage (numUsersOnline : number) {if (numUsersOnline ) {return `Il y a ${numUsersOnline } utilisateurs en ligne !`;}return "C'est désert.";}
En JavaScript, les expressions tel que if convertissent leurs conditions vers des booleans pour les comprendre, puis choisissent leur branche selon si le résultat est true ou false.
Les valeurs
0NaN""(chaîne de caractères vide)0n(zéro mais enbigint)nullundefined
sont toutes converties en false, et les autres valeurs sont converties en true.
Vous pouvez obtenir des booleans en passant ces valeurs à la fonction Boolean, ou grâce à la double négation (TypeScript pourra inférer true dans le deuxième cas, tandis qu’il se contentera de boolean dans le premier.)
tsTry// les deux renverront 'true'Boolean ("hello"); // type: boolean, value: true!!This kind of expression is always truthy.2872This kind of expression is always truthy."world" ; // type: true, value: true
Ce comportement est utilisé assez souvent, surtout pour se prémunir de valeurs tel que null ou undefined.
Essayons de l’appliquer à notre fonction printAll.
tsTryfunctionprintAll (strs : string | string[] | null) {if (strs && typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}
Vous remarquerez que nous nous sommes débarrassés de l’erreur ci-dessus en vérifiant si strs est truthy (si sa valeur booléenne est true).
Cela nous prémunit d’erreurs fatales tel que :
txtTypeError: null is not iterable
Souvenez-vous cependant que vérifier des primitives peut souvent mal se passer.
Considérez ce deuxième essai d’implémentation de printAll :
tsTryfunctionprintAll (strs : string | string[] | null) {// !!!!!!!!!!!!!!!!// Ne faites pas ça// !!!!!!!!!!!!!!!!if (strs ) {if (typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}}
Nous avons entouré tout le corps de la fonction dans une vérification de valeur truthy, mais cette approche possède un défaut subtil : on ne gère plus le cas de la chaîne de caractères vide correctement. Les développeurs moins expérimentés avec JavaScript pourraient tomber dans ce piège.
TypeScript est souvent capable de repérer les bugs existants, mais si vous décidez de ne rien faire avec une valeur, il ne peut pas non plus pallier à vos oublis sans être trop strict. Un linter, cependant, peut signaler ces cas de figure.
Une dernière remarque sur le filtrage par Boolean, c’est que le système de typage s’adaptera dans la branche appropriée et omettra les types falsy (qui ne sont, donc, pas truthy) de ses prédictions.
tsTryfunctionmultiplyAll (values : number[] | undefined,factor : number): number[] | undefined {if (!values ) {returnvalues ;} else {returnvalues .map ((x ) =>x *factor );}}
Rétrécissement par égalités
TypeScript utilise également les déclarations switch ainsi que les vérifications d’égalités, comme ===, !==, ==, et != pour rétrécir les types.
Par exemple :
tsTryfunctionexample (x : string | number,y : string | boolean) {if (x ===y ) {// On peut appeler toutes les méthodes de 'string' sur x ou y.x .toUpperCase ();y .toLowerCase ();} else {console .log (x );console .log (y );}}
Quand on a vérifié que x et y sont égaux, TypeScript a su que leurs types devaient aussi être égaux.
Vu que string est le seul type que x et y peuvent avoir, TypeScript sait que x et y doivent être des string dans la première branche.
Accomplir des vérifications contre des valeurs (et non pas des variables) fonctionne également.
Dans la section sur le rétrécissement par truthiness, la fonction printAll ne gérait pas les strings vides.
Une vérification simple pour éliminer les nulls aurait suffi pour que TypeScript soit plus précis sur le type de strs.
tsTryfunctionprintAll (strs : string | string[] | null) {if (strs !== null) {if (typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}}
Les vérifications d’égalité moins strictes de JavaScript, tel que == et !=, sont rétrécies correctement.
Si vous n’êtes pas familier, vérifier si quelque chose == null vérifie aussi si elle == undefined. De même, si quelque chose == undefined, JavaScript vérifiera aussi si elle == null.
tsTryinterfaceContainer {value : number | null | undefined;}functionmultiplyValue (container :Container ,factor : number) {// Remove both 'null' and 'undefined' from the type.if (container .value != null) {console .log (container .value );// Now we can safely multiply 'container.value'.container .value *=factor ;}}
The in operator narrowing
JavaScript has an operator for determining if an object has a property with a name: the in operator.
TypeScript takes this into account as a way to narrow down potential types.
For example, with the code: "value" in x. where "value" is a string literal and x is a union type.
The “true” branch narrows x’s types which have either an optional or required property value, and the “false” branch narrows to types which have an optional or missing property value.
tsTrytypeFish = {swim : () => void };typeBird = {fly : () => void };functionmove (animal :Fish |Bird ) {if ("swim" inanimal ) {returnanimal .swim ();}returnanimal .fly ();}
To reiterate optional properties will exist in both sides for narrowing, for example a human could both swim and fly (with the right equipment) and thus should show up in both sides of the in check:
tsTrytypeFish = {swim : () => void };typeBird = {fly : () => void };typeHuman = {swim ?: () => void;fly ?: () => void };functionmove (animal :Fish |Bird |Human ) {if ("swim" inanimal ) {animal ;} else {animal ;}}
instanceof narrowing
JavaScript has an operator for checking whether or not a value is an “instance” of another value.
More specifically, in JavaScript x instanceof Foo checks whether the prototype chain of x contains Foo.prototype.
While we won’t dive deep here, and you’ll see more of this when we get into classes, they can still be useful for most values that can be constructed with new.
As you might have guessed, instanceof is also a type guard, and TypeScript narrows in branches guarded by instanceofs.
tsTryfunctionlogValue (x :Date | string) {if (x instanceofDate ) {console .log (x .toUTCString ());} else {console .log (x .toUpperCase ());}}
Assignments
As we mentioned earlier, when we assign to any variable, TypeScript looks at the right side of the assignment and narrows the left side appropriately.
tsTryletx =Math .random () < 0.5 ? 10 : "hello world!";x = 1;console .log (x );x = "goodbye!";console .log (x );
Notice that each of these assignments is valid.
Even though the observed type of x changed to number after our first assignment, we were still able to assign a string to x.
This is because the declared type of x - the type that x started with - is string | number, and assignability is always checked against the declared type.
If we’d assigned a boolean to x, we’d have seen an error since that wasn’t part of the declared type.
tsTryletx =Math .random () < 0.5 ? 10 : "hello world!";x = 1;console .log (x );Type 'boolean' is not assignable to type 'string | number'.2322Type 'boolean' is not assignable to type 'string | number'.= true; x console .log (x );
Control flow analysis
Up until this point, we’ve gone through some basic examples of how TypeScript narrows within specific branches.
But there’s a bit more going on than just walking up from every variable and looking for type guards in ifs, whiles, conditionals, etc.
For example
tsTryfunctionpadLeft (padding : number | string,input : string) {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
padLeft returns from within its first if block.
TypeScript was able to analyze this code and see that the rest of the body (return padding + input;) is unreachable in the case where padding is a number.
As a result, it was able to remove number from the type of padding (narrowing from string | number to string) for the rest of the function.
This analysis of code based on reachability is called control flow analysis, and TypeScript uses this flow analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, control flow can split off and re-merge over and over again, and that variable can be observed to have a different type at each point.
tsTryfunctionexample () {letx : string | number | boolean;x =Math .random () < 0.5;console .log (x );if (Math .random () < 0.5) {x = "hello";console .log (x );} else {x = 100;console .log (x );}returnx ;}
Using type predicates
We’ve worked with existing JavaScript constructs to handle narrowing so far, however sometimes you want more direct control over how types change throughout your code.
To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:
tsTryfunctionisFish (pet :Fish |Bird ):pet isFish {return (pet asFish ).swim !==undefined ;}
pet is Fish is our type predicate in this example.
A predicate takes the form parameterName is Type, where parameterName must be the name of a parameter from the current function signature.
Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.
tsTry// Both calls to 'swim' and 'fly' are now okay.letpet =getSmallPet ();if (isFish (pet )) {pet .swim ();} else {pet .fly ();}
Notice that TypeScript not only knows that pet is a Fish in the if branch;
it also knows that in the else branch, you don’t have a Fish, so you must have a Bird.
You may use the type guard isFish to filter an array of Fish | Bird and obtain an array of Fish:
tsTryconstzoo : (Fish |Bird )[] = [getSmallPet (),getSmallPet (),getSmallPet ()];constunderWater1 :Fish [] =zoo .filter (isFish );// or, equivalentlyconstunderWater2 :Fish [] =zoo .filter (isFish ) asFish [];// The predicate may need repeating for more complex examplesconstunderWater3 :Fish [] =zoo .filter ((pet ):pet isFish => {if (pet .name === "sharkey") return false;returnisFish (pet );});
In addition, classes can use this is Type to narrow their type.
Discriminated unions
Most of the examples we’ve looked at so far have focused around narrowing single variables with simple types like string, boolean, and number.
While this is common, most of the time in JavaScript we’ll be dealing with slightly more complex structures.
For some motivation, let’s imagine we’re trying to encode shapes like circles and squares.
Circles keep track of their radiuses and squares keep track of their side lengths.
We’ll use a field called kind to tell which shape we’re dealing with.
Here’s a first attempt at defining Shape.
tsTryinterfaceShape {kind : "circle" | "square";radius ?: number;sideLength ?: number;}
Notice we’re using a union of string literal types: "circle" and "square" to tell us whether we should treat the shape as a circle or square respectively.
By using "circle" | "square" instead of string, we can avoid misspelling issues.
tsTryfunctionhandleShape (shape :Shape ) {// oops!if (This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.2367This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.shape .kind === "rect") {// ...}}
We can write a getArea function that applies the right logic based on if it’s dealing with a circle or square.
We’ll first try dealing with circles.
tsTryfunctiongetArea (shape :Shape ) {return'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.Math .PI *shape .radius ** 2;}
Under strictNullChecks that gives us an error - which is appropriate since radius might not be defined.
But what if we perform the appropriate checks on the kind property?
tsTryfunctiongetArea (shape :Shape ) {if (shape .kind === "circle") {return'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.Math .PI *shape .radius ** 2;}}
Hmm, TypeScript still doesn’t know what to do here.
We’ve hit a point where we know more about our values than the type checker does.
We could try to use a non-null assertion (a ! after shape.radius) to say that radius is definitely present.
tsTryfunctiongetArea (shape :Shape ) {if (shape .kind === "circle") {returnMath .PI *shape .radius ! ** 2;}}
But this doesn’t feel ideal.
We had to shout a bit at the type-checker with those non-null assertions (!) to convince it that shape.radius was defined, but those assertions are error-prone if we start to move code around.
Additionally, outside of strictNullChecks we’re able to accidentally access any of those fields anyway (since optional properties are just assumed to always be present when reading them).
We can definitely do better.
The problem with this encoding of Shape is that the type-checker doesn’t have any way to know whether or not radius or sideLength are present based on the kind property.
We need to communicate what we know to the type checker.
With that in mind, let’s take another swing at defining Shape.
tsTryinterfaceCircle {kind : "circle";radius : number;}interfaceSquare {kind : "square";sideLength : number;}typeShape =Circle |Square ;
Here, we’ve properly separated Shape out into two types with different values for the kind property, but radius and sideLength are declared as required properties in their respective types.
Let’s see what happens here when we try to access the radius of a Shape.
tsTryfunctiongetArea (shape :Shape ) {returnProperty 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'.2339Property 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'.Math .PI *shape .** 2; radius }
Like with our first definition of Shape, this is still an error.
When radius was optional, we got an error (with strictNullChecks enabled) because TypeScript couldn’t tell whether the property was present.
Now that Shape is a union, TypeScript is telling us that shape might be a Square, and Squares don’t have radius defined on them!
Both interpretations are correct, but only the union encoding of Shape will cause an error regardless of how strictNullChecks is configured.
But what if we tried checking the kind property again?
tsTryfunctiongetArea (shape :Shape ) {if (shape .kind === "circle") {returnMath .PI *shape .radius ** 2;}}
That got rid of the error! When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.
In this case, kind was that common property (which is what’s considered a discriminant property of Shape).
Checking whether the kind property was "circle" got rid of every type in Shape that didn’t have a kind property with the type "circle".
That narrowed shape down to the type Circle.
The same checking works with switch statements as well.
Now we can try to write our complete getArea without any pesky ! non-null assertions.
tsTryfunctiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;}}
The important thing here was the encoding of Shape.
Communicating the right information to TypeScript - that Circle and Square were really two separate types with specific kind fields - was crucial.
Doing that let us write type-safe TypeScript code that looks no different than the JavaScript we would’ve written otherwise.
From there, the type system was able to do the “right” thing and figure out the types in each branch of our switch statement.
As an aside, try playing around with the above example and remove some of the return keywords. You’ll see that type-checking can help avoid bugs when accidentally falling through different clauses in a
switchstatement.
Discriminated unions are useful for more than just talking about circles and squares. They’re good for representing any sort of messaging scheme in JavaScript, like when sending messages over the network (client/server communication), or encoding mutations in a state management framework.
The never type
When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left.
In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.
Exhaustiveness checking
The never type is assignable to every type; however, no type is assignable to never (except never itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a switch statement.
For example, adding a default to our getArea function which tries to assign the shape to never will raise when every possible case has not been handled.
tsTrytypeShape =Circle |Square ;functiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;default:const_exhaustiveCheck : never =shape ;return_exhaustiveCheck ;}}
Adding a new member to the Shape union, will cause a TypeScript error:
tsTryinterfaceTriangle {kind : "triangle";sideLength : number;}typeShape =Circle |Square |Triangle ;functiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;default:constType 'Triangle' is not assignable to type 'never'.2322Type 'Triangle' is not assignable to type 'never'.: never = _exhaustiveCheck shape ;return_exhaustiveCheck ;}}