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
2
3
4
5
6
7
8
9
10
11
12

{
"compilerOptions": {
"strict": true,
"noImplicitThis": false,
"alwaysStrict": false,
"strictNullChecks": false,
"strictFunctionTypes": false,
"strictPropertyInitialization": false,
}
}

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
2
3
function fn(someArg) { // ERREUR : Parameter 'someArg' implicitly has an 'any' type.
//...
}
1
2
3
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
2
3
4
5
6
{
"compilerOptions": {
"strict": true,
"suppressImplicitAnyIndexErrors": true,
}
}
  • déclarer explicitement le type de signature de l’index pour accéder aux propriétés de l’objet :
1
2
3
4
5
interface IFoo {
key1: string;
key2: number;
[key: string]: string|number;
}
  • utiliser le mot-clé keyof T (TypeScript 2.1) qui permet d’obtenir une union des nom des clés de l’objets :
1
2
3
4
5
6
7
8
9
10
11
12
interface IFoo {
key1: string;
key2: number;
}

const foo: IFoo = {
key1: 'value',
key2: 42
};

const key: (keyof IFoo) = 'key1';
const val: string = foo[key];

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
2
3
4
5
6
7
8
let farms = {
animals: ["chicken", "cow", "rabbit"],
createGroup: function() {
return function() {
return { group: this.animals }; // ERREUR : 'this' implicitly has type 'any' because it does not have a type annotation.
}
}
}

Pour corriger l’exemple ci-dessus, on peut transformer la fonction retournée par la méthode createGroup en fonction fléchée :

1
2
3
4
5
6
let farms = {
animals: ["chicken", "cow", "rabbit"],
createGroup: function() {
return () => { group: this.animals }; // TypeScript peux inférer this (type -> this: { animals: string[]; createGroup: () => () => void; })
}
}

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
2
3
function f(this: void) {
// 'this' est ici inutilisable
}

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
2
3
let foo: string = ''; // Ici on à en réalité foo: string|null|undefined
foo = null; // Pas d'erreur
foo = undefined; // Pas d'erreur

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 :

strictNullChecksImage venant de l’article Marius Schulz

Si on reprend l’exemple précédent :

1
2
3
4
5
let foo: string = '';
foo = null; // ERREUR : Type 'null' is not assignable to type 'string'.

let bar: string|null = '';
bar = null;

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
2
3
4
5
6
7
8
9
10
declare function fn(x: string): void;

let bar: string|null = '';
bar = null;

if (bar) {
fn(bar)
} else {
fn(bar) // ERREUR : Argument of type 'null' is not assignable to parameter of type 'string'.
}

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
2
3
4
5
6
7
8
function validateObject(o?: Object) {
// Ici on check que l'objet n'est ni null ni undefined
}

function foo(o?: Object) {
validateObject(o);
const bar = o!.prop; // Dans la fonction précédente nous avons déjà vérifié que la variable n'est ni null ni undefined
}

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
2
3
4
5
6
7
8
9
10
11
12
13
class Vegetable {}
class Garlic extends Vegetable {
nbClove: string;
}
class GreenBean extends Vegetable {
nbBean: string;
}

function cookGarlic(g: Garlic) { };
function cloneVegetable(source: Vegetable, done: (result: Vegetable) => void): void { };
let b = new Bean();

cloneAnimal(b, cookGarlic); // ERREUR, empêche d'utiliser cookGarlic avec un GreenBean
1
2
3
4
5
6
function makeLowerCase(s: string) {
return s.toLowerCase();
}

declare let foo: Promise<string | number>;
foo.then(makeLowerCase); // ERREUR, empêche d'appeler makeLowerCase avec un number

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
2
3
4
5
6
7
8
9
class C {
foo: number;
bar = "hello";
baz: boolean; // ERROR

constructor() {
this.foo = 42;
}
}

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
2
3
4
5
6
7
8
9
class C {
foo!: number; // &lsaquo;-- definite assignment assertion

constructor() {}

onInit() {
this.foo = 10;
}
}

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 !