BLOG

JS Static Analysis Part 3 - Using Flow

This is Part 3 of a 4 part intro series to static analysis tooling for JavaScript:

Basic Flow Usage

flow Documentation

Flow is a library from Facebook that does static checking of your JavaScript code, similar to what a linter would do, but with more capability to make smart decisions by understanding your code at a deeper level.

Let’s get started. We need to install the binary and create an empty .flowconfig file:

> npm install -g flow-bin
> touch .flowconfig

Now let’s create a JavaScript file and see what happens. Note that you have to put the /* @flow */ at the top of any file that you want flow to recognize. This will help you migrate your code slowly. If you’d rather bypass this you can run flow with flow check --all.

/* @flow */
function addTwo(first, second) {
  return
  a = 1
}

In your terminal type:

> flow
a.js:4
  4:   a = 1
       ^^^^^ unreachable code


Found 1 error

Just like JSLint, right out of the box we’ve found flow useful. We had to add /* @flow */ to our source file, but that’s it. Our project and source code is relatively untouched.

Let’s try to find other things flow can find for us:

/* @flow */
function getLength(str) {
  return str.length;
}
getLength('michael');
getLength(null);

Now run flow again in your terminal:

> flow
a.js:6
  6: getLength(null);
     ^^^^^^^^^^^^^^^ function call
  3:   return str.length;
                  ^^^^^^ property `length`. Property cannot be accessed on possibly null value
  3:   return str.length;
              ^^^ null

This is where the power of flow over a normal linter starts to shine. Here it’s smart enough to realize that we passed null to a function that will try to return the length of the variable passed in. Again, we haven’t modified our source at all other than a comment on the first line.

Let’s take this a step further:

/* @flow */
function getLength(str) {
  if (str != null) { // Check using != so it captures `undefined` as well
    return str.length;
  }
  return 0;
}
getLength('michael');
getLength(null);
getLength();

And now if you run flow:

> flow
No errors!

Here flow is smart enough to parse our getLength function and determine that we’re checking for null (or undefined) before returning the length. So now it passes with no errors.

This doesn’t just work for built-in types. You can also use it for custom objects:

/* @flow */
function getLength(str) {
  if (str != null) {
    return str.length;
  }
  return 0;
}

var myObjNoLength = {
  a: 1
};

var myObjWithLength = {
  length: 4
}

getLength(myObjWithLength);
getLength(myObjNoLength);

Here it’s smart enough to realize we have custom object that does not have a length property, even though it will pass our null check:

> flow
a.js:4
  4:     return str.length;
                    ^^^^^^ property `length`. Property not found in
  4:     return str.length;
                ^^^ object literal


Found 1 error

Adding Type Annotations

The basic usage out of the box is nice, but similar to TypeScript, flow expects that you’ll want to start annotating your code with static types, so that it can provide additional help. To do that however, you’ll need to add a built step before you deploy, since you’ll be creating code that can’t run as-is in either a browser or in node.js.

Let’s take the same code above and try requiring that we pass in a string:

/* @flow */
function getLength(str: string) { // Added annotation here
  if (str && str.length) {
    return str.length;
  }
  return 0;
}

var myObjWithLength = {
  length: 4
}

console.log(getLength('michael')); // OK
console.log(getLength(myObjWithLength)); // Not a string

Then when running this with flow, it will detect that we’re passing in an object. Even though that object has a length property, it is not a string.

> flow
a.js:14
 14: console.log(getLength(myObjWithLength)); // Not a string
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^ function call
 14: console.log(getLength(myObjWithLength)); // Not a string
                           ^^^^^^^^^^^^^^^ object literal. This type is incompatible with
  2: function getLength(str: string) {
                             ^^^^^^ string


Found 1 error

So we have some better type checking, but now we have to include a build process. If we leave things as-is, we can’t run it:

> node a.js
./demo/a.js:2
function getLength(str: string) {
                      ^

SyntaxError: Unexpected token :
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:373:25)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:974:3

flow gives us two ways of dealing with this. The simplest is installing flow-remove-types. All this does is traverse through the code and strip any annontations it finds.

> npm install flow-remove-types --save-dev
> .flow-remove-types a.js > output.js
> node output.js
7
4

If you look at the contents of output.js, you’ll see it has a very simplistic removal of things related to flow:

/*       */
function getLength(str        ) {
  if (str && str.length) {
    return str.length;
  }
  return 0;
}

var myObjWithLength = {
  length: 4
}

console.log(getLength('michael')); // OK
console.log(getLength(myObjWithLength)); // Not a string, still runs

Or, we can use babel and the babel plugin to accomplish the same thing. This is especially helpful if babel is already part of your existing build process.

> npm install -g babel-cli
> npm install --save-dev babel-plugin-transform-flow-strip-types
> echo '{"plugins": ["transform-flow-strip-types"]}' > .babelrc
> babel-node a.js
7
4

Help with 3rd Party Libraries

What would really be nice is if we can get this same checking we get for our own source code in 3rd party libraries we use. To help with this, flow uses a package called flow-typed. Once you’ve installed your dependencies with a normal npm install, you can run flow-typed install to have it scan your node_modules for any typings files that can be found.

> npm install -g flow-typed
> npm install pad --save
> flow-typed install
• Found flow-bin@v0.33.0 installed. Installing libdefs compatible with this version of Flow...
• Found 4 dependencies in package.json. Searching for libdefs...
• rebasing flow-typed cache...done.
• Installing 2 libdefs...
  • flow-bin_v0.x.x.js
    └> ./flow-typed/npm/flow-bin_v0.x.x.js
  • pad_v1.x.x.js
    └> ./flow-typed/npm/pad_v1.x.x.js
• Generating stubs for untyped dependencies...
  • babel-plugin-transform-flow-strip-types@^6.14.0
    └> flow-typed/npm/babel-plugin-transform-flow-strip-types_vx.x.x.js
  • flow-remove-types@^1.0.4
    └> flow-typed/npm/flow-remove-types_vx.x.x.js

!! No flow@v0.33.0-compatible libdefs found in flow-typed for the above untyped dependencies !!

   I've generated `any`-typed stubs for these packages, but consider submitting
   libdefs for them to https://github.com/flowtype/flow-typed/

Now we can get some type checking with 3rd party modules:

/* @flow */
var pad = require('pad');
pad(5, 'pad', '--')
pad(true, '--'); // will generate an error

But first, before we run flow, let’s exclude it from checking our node_modules folder:

.flowconfig
[ignore]
.*/node_modules/.*

Now back in your terminal:

> flow
a.js:4
  4: pad(true, '--');
     ^^^^^^^^^^^^^^^ function call
  4: pad(true, '--');
         ^^^^ boolean. This type is incompatible with
 12:     textOrLeftPadding: string|number,
                            ^^^^^^^^^^^^^ union: string | number. See lib: flow-typed/npm/pad_v1.x.x.js:12
  Member 1:
   12:     textOrLeftPadding: string|number,
                              ^^^^^^ string. See lib: flow-typed/npm/pad_v1.x.x.js:12
  Error:
    4: pad(true, '--');
           ^^^^ boolean. This type is incompatible with
   12:     textOrLeftPadding: string|number,
                              ^^^^^^ string. See lib: flow-typed/npm/pad_v1.x.x.js:12
  Member 2:
   12:     textOrLeftPadding: string|number,
                                     ^^^^^^ number. See lib: flow-typed/npm/pad_v1.x.x.js:12
  Error:
    4: pad(true, '--');
           ^^^^ boolean. This type is incompatible with
   12:     textOrLeftPadding: string|number,
                                     ^^^^^^ number. See lib: flow-typed/npm/pad_v1.x.x.js:12

This is a bit hard to read, but accomplishes the goal. Line 4 calls pad(true, '--'), which is not a valid format for this library.

Unfortunately, as of October 2016, the number of typings files available is still pretty small. You can see the full list here:

flowtype/flow-typed

calendartwitterfeedenvelopelinkedingithub-altbitbucket