In this article, we will talk about automatic checking of types in Javascript, to avoid unexpected behavior at runtime and to simplify refactoring and testing. Javascript has been created as an auxiliary language for use in static HTML documents in web pages. 20 years later, most web pages are real applications, composed by HTML documents dynamically generated by Javascript code. This does not change the fact that Javascript is a dynamic and weak typing language.

Typing is called Dynamic, because the types of variables and functions are only known at runtime. This feature is illustrated by the following example.

function log(value) {
    console.log(Date.now(), value);
}
log("Hello world!");

In this example, the type of the variable value is only known at runtime. In the example, we passed a string of characters in the parameters, but we could also pass a digit or a boolean. The typing is called weak, because even though each value has a well-defined type, the types of the variables may change while running.

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

In this example, the variable value has the string type before the first log call, and number in the second call. Another example of weak and dynamic typing is given in the following example.

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)]));

In this example, the totalLegs function calculates the sum of leg numbers received in the input array. In the usage example, we can see that the function works with objects of type Table, and any object with a property legs. All these features make Javascript code very easy to write, but very difficult to maintain. When writing a function, one must document the types of input and output. In the example, the animal input variable has been called to imply that an object of the Animal type is expected, but this does not prevent the user from calling the function with objects of a different type, as shown in the example. With or without documentation, the only way to know exactly what a function takes as input / output is to read the function code. The developer manually checks all the code of the application to estimate the impact of a modification. Javascript code can be made easier to maintain by writing tests.

function shouldComputeTotalLegs() {
    let animals = [
        new Animal('Peter', 2),
        new Animal('Tiger', 4)
    ];
    let total = totalLegs(animals);
    assert(total.numberOfLegs === 6);
}

The disadvantage of tests is that it is difficult to predict all possible cases.

Type checking in a nutshell

Type verification tools can be seen as sequenced tests run automatically. The difference is that the code is not executed, but statically verified (before any compilation), which makes it possible to identify a larger number of errors.

In order for it to be used, the developer must modify and add to the code with the types of the variables. The two most commonly used type checking engines are Flow (https://flow.org) created by Facebook and Typescript (https://www.typescriptlang.org/) created by Microsoft. The syntax supported by each engine is very similar. The following code has been modified with types. It is valid in Typescript and Flow.

In this example, we use the primitive number and string types and we define the complex type TotalLegs and the Array type that represents the array type of Animal objects.

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)]));

With annotation, the Typescript and Flow engines can find typing errors automatically, without having to write any tests. In the following example, we pass an element that does not have the Animal type (as in the previous example) to the totalLegs function; the engines will detect the non-compliance of the specified typing.

console.log(totalLegs([new Animal('Peter', 2), new Table(12, 147)]));
Out Typescript’s engine
Out typescript’s flow

The most important difference between Flow and Typescript is that the first is just a .js file checker while the second is a transpiler. Concretely, we use Flow with a transpiler such as babel to generate code in an old version of Javascript. When using Typescript, we create .ts files and we use Typescript to generate Javascript files after verification. Flow can therefore afford to be much stricter with typing errors than Typescript. It is able to check for non-added errors with types, Typescript supports only annotation added iteratively, and therefore verifications are done iteratively.

For further knowledge

In this article we recalled that Javascript is a dynamic and weak typing language. This simplifies the creation of simple sites, but makes complicated the creation of modern applications, with thousands of lines of code. It is difficult to predict the impact of changes in the code, and write tests to be able to do them automatically. The following article quantifies the benefits of static checking of types in Javacript. It shows that the use of Flow or Typescript avoids around 15% of bugs.