TypeScript Best Practices : option strict
Voilà maintenant plus de 5 ans que j’arpente les sillages du typage static en Javascript grâce à TypeScript. Étant donné le peu de ‘Style Guide’, je me permet de lancer une série d’articles qui auront pour but de parler des Best Practices pour assurer le plus haut niveau de contrôle des types. On commence tout de suite avec l’option de compilation la plus indispensable : strict.
Option de compilation strict ?
Apparue avec la sortie de TypeScript 2.3, c’est ce qu’on apelle une option ‘maître’. En effet, lorsque cette option est mise à true dans votre tsconfig.json, elle active un certain nombre de sous-options de compilation qui renforce le contrôle des types. Voici la liste des sous-options activées :
- noImplicitAny
- noImplicitThis
- alwaysStrict
- strictNullChecks
- strictFunctionTypes
- strictPropertyInitialization
Si strict n’est pas dans votre tsconfig.json, alors elle est désactivée par défaut. Si vous créez votre tsconfig.json avec l’invite de commande tsc –init, alors le tsconfig.json généré aura par défaut l’option activée.
Un point important à retenir et qu’il est possible d’utiliser l’option strict de façon incrémentale, par exemple :
1 |
|
Ici seul une des sous-options est activée (noImplicitAny). Ceci est très utile lorsque vous activez l’option strict sur un projet qui à un lourd passif et que vous souhaitez corriger les erreurs remonter par les sous-options au fur et à mesure. Vous pouvez donc les activer une à une en retirant la ligne qui la désactive.
Décortiquont maintenant chacune de ces fameuses sous-options afin de mieux comprendre leur utilité.
noImplicitAny (TypeScript 1.0)
Cette option existe depuis la première relase de TypeScript ! Elle lève une erreur lorsqu’un élément (variable, retour de fonction, …) est typé en any de façon implicite. TypeScript met le type any par défaut (donc de façon implicite) si :
- le compilateur ne peut inférer le type
- le type n’est pas explicitment précisé
Exemple :
1 | function fn(someArg) { // ERREUR : Parameter 'someArg' implicitly has an 'any' type. |
1 | function fn(someArg: any) { // Ici pas d'erreur car someArgs est typé en any de façon explicite |
Cas particulier : l’erreur -> Index signature of object type implicitly has an ‘any’ type
TypeScript lève cette erreur lorsque l’option noImplicitAny est activée et que vous tentez d’acceder à une propriété d’un objet via son index (obj[prop]). Par default TypeScript ne peux inférer l’index de l’objet. Il existe aors plusieurs solutions pour ne plus avoir ce message d’erreur :
- ajouter dans le tsconfig.json suppressImplicitAnyIndexErrors=true ce qui empêchera la remonté de ces erreurs :
1 | { |
- déclarer explicitement le type de signature de l’index pour accéder aux propriétés de l’objet :
1 | interface IFoo { |
- utiliser le mot-clé keyof T (TypeScript 2.1) qui permet d’obtenir une union des nom des clés de l’objets :
1 | interface IFoo { |
La dernière solution est pour moi la plus élégante.
noImplicitThis (TypeScript 2.0)
Le this en Javascript est assez particulier et il est facile de tomber dans des pièges. Cette option ‘catch’ les utilisations de this potentiellement dangereuse. Si TypeScript n’arrive pas à inférer this et qu’il n’est pas explicitement typé alors l’option noImplicitThis lève une erreur.
Exemple :
1 | let farms = { |
Pour corriger l’exemple ci-dessus, on peut transformer la fonction retournée par la méthode createGroup en fonction fléchée :
1 | let farms = { |
Avec TypeScript il est possible de typer le this à l’entrée d’une fonction. Cela peut avoir plusieurs utilités, notamment dans le cas où TypeScript n’arrive pas l’inférer ou si l’on souhaite empêcher son utilisation comme dans l’exemple ci-dessous :
1 | function f(this: void) { |
strictNullChecks (TypeScript 2.0)
Grosse option mise en l’avant lors de la sortie de TS 2.0, elle est inspirée du fonctionnement de Flow. En TypeScript et sans l’option strictNullChecks activée, null et undefined ne sont pas des types à part entier et peuvent être assignés à tous les types. Par exemple :
1 | let foo: string = ''; // Ici on à en réalité foo: string|null|undefined |
Donc lorsque vous accédez à une variable, vous ne savez pas si elle est potentiellement null ou undefined. En activant l’option strictNullChecks, null/undefined deviennent des types à part entier et doivent être préciser explicitement :
Image venant de l’article Marius Schulz
Si on reprend l’exemple précédent :
1 | let foo: string = ''; |
L’intêret de cette option et de vous certifier que votre variable n’est ni null ni undefined au moment ou vous y accéder. Vous pouvez utiliser les types guards pour tester votre variable :
1 | declare function fn(x: string): void; |
On peut contrecarrer le compilateur en utilisant l’opérateur Non-Null Assertion Operator alias ‘!’. A utiliser avec parcimonie, car lors de son utilisation, le compilo de TypeScript nous fait confiance et ne check plus le fait que la variable soit null ou undefined :
1 | function validateObject(o?: Object) { |
alwaysStrict (TypeScript 2.1)
Pour comprendre cette option, il faut connaître la directive “use strict” apparue avec l’implémentation de l’EcmaScript 5. Grosso modo, placer cette directive en haut d’un fichier indique au moteur JS qui l’interprétera, qu’il doit utiliser le strict mode. Dans ce mode, beaucoup d’erreurs qui avant étaient silcencieuse, lèvent ici une exception.
L’option alwaysStrict permet d’inclure cette directive dans tout vos fichiers et être sur que tout votre code sera interpréter en mode strict.
Depuis es2015 les modules et classes sont automatiquement interprétés en mode strict.
strictFunctionTypes (TypeScript 2.6)
Pour un rappel sur la covariance/contravariance/invariance/bivariance c’est ici. Inspirée du fonctionnement de Flow, cette option permet de checker les paramètres de fonction de façon contravariant. Cela permet de remonter ce genre d’erreur :
1 | class Vegetable {} |
1 | function makeLowerCase(s: string) { |
Ne s’applique pas aux méthodes, ni aux constructeurs.
strictPropertyInitialization (TypeScript 2.7)
Cette option comme son nom l’indique, force l’initialisation des champs d’une classe soit dans le constructeur ou directement lors de la déclaration du champs. Exemple :
1 | class C { |
Cas particulier : cette option peut poser problème lorsque vous utilisez un framework ou l’on initialise pas les champs dans le constructeur. Par exemple avec Angular, React,… On peut initialiser des champs lors du cycle d’initialisation du composant. Pour ce cas particulier, il existe un opérateur spécial pour dire au compilo de TypeScript que l’on à initilisé ce membre : ! alias ‘definite assignment assertion’ (à ne pas confondre avec Non-Null Assertion Operator ^^). Voici un exemple d’utilisation :
1 | class C { |
Et voilà, j’éspère que cette article vous à donné envie d’utiliser l’option strict sur tout vos projet TypeScript. Elle aporte vraiment une grosse plus value en terme de contrôle des types !
Dernière petite choses avant qu’on se quitte, étant donné que les nouvelles versions de TypeScript vont régulièrement inclure de nouvelles sous-options, n’oubliez pas en premier lieu de :
- désactivez ces sous-options lors de la mise à jour
- bien comprendre ce qu’elle font
- les activer et corriger les errreurs qu’elles remontent :)
A très vite !