JavaScript - - By Nilson Jacques

Writing Better JavaScript with Flow

How often have you found yourself tracking down a bug in some code, only to find the error was something simple that should have been avoidable? Maybe you passed the arguments to a function in the wrong order, or perhaps you tried to pass a string instead of a number? JavaScript’s weak typing system and willingness to try to coerce variables into different types can be a source of a whole class of bugs that just don’t exist in statically typed languages.

March 30th, 2017: The article was updated to reflect changes to the Flow library.

Flow is a static type checker for JavaScript first introduced by Facebook at the Scale Conference in 2014. It was conceived with a goal of finding type errors in JavaScript code, often without having to modify our actual code, hence consuming little effort from the programmer. At the same time, it also adds additional syntax to JavaScript that provides more control to the developers.

In this article, I’ll introduce you to Flow and it’s main features. We’ll look at how to set it up, how to add type annotations to your code, and how to automatically strip out those annotations when running the code.

Installation

Flow currently works on Mac OS X, Linux (64-bit) and Windows (64-bit). The easiest way to install it is via npm:

npm install --save-dev flow-bin

and add it to your project’s package.json file, under the scripts section:

"scripts": {
  "flow": "flow"
}

Once this is done, we’re ready to go ahead and explore its features.

Getting Started

A configuration file named .flowconfig must be present at the root of the project folder. We can create an empty config file by running the command:

npm run flow init

Once the config file is present, you can run ad-hoc checks on the code within your project folder and any subfolders by running the following command at the terminal:

npm run flow check

However, this is not the most efficient way to use Flow since it causes Flow itself to recheck the entire project’s file structure every time. We can use the Flow server, instead.

The Flow server checks the file incrementally which means that it only checks the part that has changed. The server can be started by running on the terminal the command npm run flow.

The first time you run this command, the server will start and show the initial test results. This allows for a much faster and incremental workflow. Every time you want to know the test results, run flow on the terminal. After you’re done with your coding session, you can stop the server using npm run flow stop.

Flow’s type checking is opt-in. This means that you don’t need to check all your code at once. You can select the files you want to check and Flow will do the job for you. This selection is done by adding @flow as a comment at the top of any JavaScript files you want to be checked by Flow:

/*@flow*/

This helps a lot when you’re trying to integrate Flow into an existing project as you can choose the files that you want to check one by one and resolve any errors.

Type Inference

Generally, type checking can be done in two ways:

  • Via annotations: We specify the types we expect as part of the code, and the type checker evaluates the code based on those expectations
  • Via code inference: The tool is smart enough to infer the expected types by looking at the context in which variables are used and checks the code based on that

With annotations, we have to write some extra code which is only useful during development and is stripped off from the final JavaScript build that will be loaded by the browser. This requires a bit of extra work upfront to make the code checkable by adding those extra type annotations.

In the second case, the code is already ready for being tested without any modification, hence minimizing the programmer’s effort. It doesn’t force you to change how you code as it automatically deduces the data type of the expressions. This is known as type inference and is one of the most important features of Flow.

To illustrate this feature, we can take the below code as an example:

/*@flow*/

function foo(x) {
  return x.split(' ');
}

foo(34);

This code will give an error on the terminal when you run the npm run flow command, as the function foo() expects a string while we have passed a number as an argument.

The error will look something like this:

index.js:4
  4:   return x.split(' ');
                ^^^^^ property `split`. Property not found in
  4:   return x.split(' ');
              ^ Number

It clearly states the location and the cause of the error. As soon as we change the argument from a number to any string, as shown in the following snippet, the error will disappear.

/*@flow*/

function foo(x) {
  return x.split(' ');
};

foo('Hello World!');

As I said, the above code won’t give any errors. What we can see here is that Flow understands that the split() method is only applicable to a string, so it expects x to be a string.

Nullable Types

Flow treats null in a different way compared to other type systems. It doesn’t ignore null, thus it prevents errors that may crash the application where null is passed instead of some other valid types.

Consider the following code:

/*@flow*/

function stringLength (str) {
  return str.length;
}

var length = stringLength(null);

In the above case, Flow will throw an error. To fix this, we’ll have to handle null separately as shown below:

/*@flow*/

function stringLength (str) {
  if (str !== null) {
    return str.length;
  }

  return 0;
}

var length = stringLength(null);

We introduce a check for null to ensure that the code works correctly in all cases. Flow will consider this last snippet as a valid code.

Type Annotations

As I mentioned above, type inference is one of the best features of Flow, as we can get useful feedback without having to write type annotations. However, in some cases, adding annotations to the code is necessary to provide better checking and remove ambiguity.

Consider the following:

/*@flow*/

function foo(x, y){
  return x + y;
}

foo('Hello', 42);

Flow won’t find any errors in the above code because the + (plus) operator can be used on strings and numbers, and we didn’t specify that the parameters of add() must be numbers.

In this case, we can use type annotations to specify the desired behavior. Type annotations are prefixed with a : (colon) and can be placed on function parameters, return types, and variable declarations.

If we add type annotations to the above code, it becomes as reported below:

/*@flow*/

function foo(x : number, y : number) : number {
  return x + y;
}

foo('Hello', 42);

This code shows an error because the function expects numbers as arguments while we’re providing a string.

The error shown on the terminal will look like the following:

index.js:7
  7: foo('Hello', 42);
         ^^^^^^^ string. This type is incompatible with the expected param type of
  3: function foo(x : number, y : number) : number{
                      ^^^^^^ number

If we pass a number instead of 'Hello', there won’t be any error. Type annotations are also useful in large and complex JavaScript files to specify the desired behavior.

With the previous example in mind, let’s have a look at the various other type annotations supported by Flow.

Functions

/*@flow*/

/*--------- Type annotating a function --------*/
function add(x : number, y : number) : number {
  return x + y;
}

add(3, 4);

The above code shows the annotation of a variable and a function. The arguments of the add() function, as well as the value returned, are expected to be numbers. If we pass any other data type, Flow will throw an error.

Arrays

/*-------- Type annotating an array ----------*/
var foo : Array<number> = [1,2,3];

Array annotations are in the form of Array<T> where T denotes the data type of individual elements of the array. In the above code, foo is an array whose elements should be numbers.

Classes

An example schema of class and object is given below. The only aspect to keep in mind is that we can perform an OR operation among two types using the | symbol. The variable bar1 is annotated with respect to the schema of the Bar class.

/*-------- Type annotating a Class ---------*/
class Bar{
  x:string;           // x should be string       
  y:string | number;  // y can be either a string or a number
  constructor(x,y){
    this.x=x;
    this.y=y;
  }
}

var bar1 : Bar = new Bar("hello",4);

Object literals

We can annotate object literals in a similar way to classes, specifying the types of the object’s properties.

/*--------- Type annonating an object ---------*/

var obj : {a : string, b : number, c: Array<string>, d : Bar} = {
  a : "hello",
  b : 42,
  c : ["hello", "world"],
  d : new Bar("hello",3)
}

Null

Any type T can be made to include null/undefined by writing ?T instead of T as shown below:

/*@flow*/

var foo : ?string = null;

In this case, foo can be either a string or null.

We’re just scratching the surface of Flow’s type annotation system here. Once you get comfortable with using these basic types, I suggest delving into the types documentation on Flow’s website.

Library Definitions

We often face situations where we have to use methods from third-party libraries in our code. Flow will throw an error in this case but, usually, we don’t want to see those errors as they will distract from checking our own code.

Thankfully, we don’t need to touch the library code to prevent these errors. Instead, we can create a library definition (libdef). A libdef is just a fancy term for a JavaScript file that contains declarations of the functions or the methods provided by the third-party code.

Let’s see an example to better understand what we’re discussing:

/* @flow */

var users = [
  { name: 'John', designation: 'developer' },
  { name: 'Doe', designation: 'designer' }
];

function getDeveloper() {
  return _.findWhere(users, {designation: 'developer'});
}

This code will give the following error:

interfaces/app.js:9
  9:   return _.findWhere(users, {designation: 'developer'});
              ^ identifier `_`. Could not resolve name

The error is generated because Flow doesn’t know anything about the _ variable. To fix this issue we need to bring in a libdef for Underscore.

Using flow-typed

Thankfully, there is a repository called flow-typed which contains libdef files for many popular third-party libraries. To use them, you simply need to download the relevant definition into a folder named flow-typed within the root of your project.

To streamline the process even further, there is a command line tool available for fetching and installing libdef files. It’s installed via npm:

npm install -g flow-typed

Once installed, running flow-typed install will examine your project’s package.json file and download libdefs for any dependencies it finds.

Creating custom libdefs

If the library you’re using doesn’t have a libdef available in the flow-typed repository, it’s possible to create your own. I won’t go into details here, as it’s something you shouldn’t need to do very often, but if you’re interested you can check out the documentation.

Stripping the Type Annotations

As type annotations are not valid JavaScript syntax, we need to strip them from the code before executing it in the browser. This can be done using the flow-remove-types tool or as a Babel preset, if you’re already using Babel to transpile your code. We’ll only discuss the first method in this article.

First, we need to install flow-remove-types as a project dependency:

npm install --save-dev flow-remove-types

Then we can add another script entry to our package.json file:

"scripts": {
  "flow": "flow",
  "build": "flow-remove-types src/ -D dest/",
}

This command will strip all the type annotations from the files present in the src folder and store the compiled version in the dist folder. The compiled files can be loaded on the browser just like any other JavaScript file.

There are plugins available for several module bundlers to strip annotations as part of the build process.

Conclusions

In this article, we discussed the various type checking features of Flow and how they can help us catch errors and improve the quality of our code. We also saw how Flow makes it very easy to get started by ‘opting in’ on a per-file basis, and doing type inference so we can start getting useful feedback without having to add annotations throughout our code,

How do you feel about static type checking for JavaScript? Is this something you can see being useful, or just another unnecessary tool bringing more complexity to modern JavaScript? Has this article encouraged you to check out Flow for yourself? Feel free to share your thoughts, doubts, or comments below.

Sponsors