Dans cet article, nous parlerons de la vérification automatique de types en Javascript, pour s’éviter des comportements inattendus à l’exécution et pour simplifier le refactoring et les tests. Javascript a été créé comme un langage auxiliaire à utiliser dans des documents statiques HTML dans des pages web. 20 ans plus tard, la plupart des pages web sont des véritables applications, composées par des documents HTML générés dynamiquement par du code Javascript. Cela ne change pas le fait que le Javascript est un langage de typage dynamique et faible.

Le typage est dit Dynamique, parce que les types des variables et des fonctions ne sont connus qu’à l’exécution. Cette caractéristique est illustrée par l’exemple suivant.

function log(value) {
    console.log(Date.now(), value);
}

log("Hello world!");

Dans cet exemple, le type de la variable value n’est connu qu’à l’exécution. Dans l’exemple, on a passé une chaîne de caractères en paramètre, mais on aurait pu passer aussi un chiffre ou un boolean. Le typage est dit Faible, parce que, même si chaque valeur a un type bien défini, les types des variables peuvent changer en cours d’exécution.

function log(value) {
    console.log(Date.now() + ": " + value);
}
var value = 5;
log(value);

value = "Hello World";
log(value);

Dans cet exemple, la variable value a le type string  avant le premier appel log, et number dans le deuxième appel. Un autre exemple de typage faible et dynamique est donné dans l’exemple suivant.

function totalLegs(animals) {
    return {
        numberOfLegs: animals.reduce((previous, current) => previous + current.legs, 0)
    };
}

class Animal {
    constructor(name, legs) {
        this.name = name;
        this.legs = legs;
    }
}

class Table {
    constructor(x, y) {
        this.legs = 4;
        this.position = {
            x, y
        };
    }
}
console.log(totalLegs([new Animal('Peter', 2), new Table(12, 147)]));

Dans cet exemple la fonction totalLegs calcule la somme des nombres de legs reçus dans le tableau d’entrée. Dans l’exemple d’utilisation, on voit que la fonction marche avec des objets du type Table, et tout objet avec une propriété legs. Toutes ces caractéristiques font que du code Javascript est très facile à écrire, mais très difficile à maintenir. Quand on écrit une fonction, on doit documenter les types d’entrée et de sortie. Dans l’exemple, on a appelé la variable d’entrée animal pour signifier que l’on s’attend à un objet du type Animal, mais cela n’empêche pas l’utilisateur d’appeler la fonction avec des objets de type différent, comme dans l’exemple. Avec ou sans documentation, le seul moyen de savoir exactement ce qu’une fonction prend en entrée/produit en sortie est de lire le code de la fonction. Au développeur de vérifier manuellement tout le code de l’application pour estimer l’impact d’une modification. On peut rendre le code Javascript plus facile à maintenir en écrivant des tests.

function shouldComputeTotalLegs() {
    let animals = [
        new Animal('Peter', 2),
        new Animal('Tiger', 4)
    ];

    let total = totalLegs(animals);
    assert(total.numberOfLegs === 6);
}

L’inconvénient des tests c’est qu’il est difficile de prévoir tous les cas possibles.

Type checking in a nutshell

Les outils de vérification de types peuvent être vus comme des suites de tests exécutés automatiquement. La différence est que le code n’est pas exécuté, mais vérifié statiquement (avant toute compilation), ce qui permet d’identifier un nombre plus grand d’erreurs. Pour s’en servir, le développeur doit annoter le code avec les types des variables. Les deux moteurs de vérification de types les plus utilisés actuellement sont Flow (https://flow.org) créé par Facebook et Typescript (https://www.typescriptlang.org/) créé par Microsoft. La syntaxe supportée par chaque moteur est très similaire. Le code suivant a été annoté avec des types. Il est valable en Typescript et en Flow. Dans cet exemple, nous utilisons les types primitifs number et string et nous définissons le type complexe TotalLegs et le type Array qui représente le type tableau d’objets Animal.

type TotalLegs = { numberOfLegs: number }

function totalLegs(animals: Array<Animal>): TotalLegs {
    return {
        numberOfLegs: animals.reduce((previous, current) => previous + current.legs, 0)
    };
}

class Animal {
    name: string;
    legs: number;
    constructor(name: string, legs: number) {
        this.name = name;
        this.legs = legs;
    }
}

class Table {
    legs: number;
    position: {
        x: number,
        y: number
    }
    constructor(x: number, y: number) {
        this.legs = 4;
        this.position = {
            x, y
        };
    }
}
console.log(totalLegs([new Animal('Peter', 2)]));

Avec des annotations, les moteurs Typescript et Flow peuvent trouver des erreurs de typage automatiquement, sans que l’on ait à écrire des tests. Dans l’exemple suivant, on passe un élément qui n’a pas le type Animal (comme dans l’exemple précédent) à la fonction totalLegs, les moteurs vont détecter le non-respect du typage indiqué.

console.log(totalLegs([new Animal('Peter', 2), new Table(12, 147)]));

Sortie du moteur Typescript

Sortie du moteur Flow

La différence la plus importante entre Flow et Typescript est que le premier est juste un vérificateur de fichiers .js tandis que le deuxième est un transpileur. Concrètement, on utilise Flow avec un transpileur tel que babel  pour générer du code en une ancienne version de Javascript . Quand on utilise Typescript, on  crée des fichiers .ts et on utilise Typescript pour générer des fichiers Javascript après vérification. Flow peut donc se permettre d’être beaucoup plus strict avec des erreurs de typage que Typescript. Il est capable de vérifier des erreurs non  annotés avec des types, Typescript supporte que des annotations ajoutées itérativement, et donc que les vérifications soient faites itérativement.

Pour aller plus loin

Dans cet article nous avons rappelé que Javascript est un langage de typage dynamique et faible. Ce qui simplifie la création de sites simples, mais rends compliqué la création d’applications modernes, avec des milliers de lignes de code.  Il est difficile de prévoir l’impact des modifications dans le code, et d’écrire des tests pour pouvoir les faire automatiquement. L’article suivant quantifie les avantages de la vérification statique de types en Javacript. Il montre que l’usage de Flow ou Typescript évitent au tour de 15% des bugs.

Dans cet article, nous avons aussi vu que la syntaxe utilisée par chaque moteur est similaire, mais leurs scénarios d’utilisation est différent, et que chaque outils s’insère dans un écosystème différent. Pour mieux comprendre les différences entre Typescript et Flow (surtout syntaxiques):

Finalement, pour aller plus loin, je recommande la lecture des retours d’expérience de l’adoption de la vérification de types, pour mieux comprendre les contraintes derrière chaque option.