Should You Static Type Check Your Javascript?

The pros and cons of using a static type checker for Javascript.

Javascript is dynamically typed: it performs type checking at runtime. On the other hand, a statically typed language like C performs type checking at compile time. Allow me to illustrate the difference. Here’s some simple C code:

#include <stdio.h>

// Adds one to an integer.
int addOne(int x) {
  return x + 1;
}

int main() {
  const char *value = "2";
  printf("%d", addOne(value)); // ?
}

That code doesn’t compile cleanly:

warning: incompatible pointer to integer conversion passing 'const char *' to parameter of type 'int'

The C compiler type checks our code and sees that we’re trying to do something weird: pass a const char * (basically, a string) to a function that expects an int. We’d catch this problem immediately and fix it.

Now, here’s some similar Javascript code:

// Add one to (hopefully) a number.
const addOne = x => x + 1;

// Let me declare this object! REMINDER: value is a string.
const object = { value: '2' };

// A bunch more code here...
// ...
// So much code that I forgot exactly what was in object...
// ...

// object.value is a number...right?
console.log(addOne(object.value)); // 21

This code works perfectly fine (even though it doesn’t do what we intended) because '2' + 1 is '21' in Javascript. Sometimes we’d catch this bug immediately, but other times bugs like this could go unnoticed for a long time.

Here’s another type of bug that happens a lot:

const Constants = Object.freeze({
  someKey: 'value',
  someOtherKey: 'otherValue',
});

// ...

const string = `Welcome to ${Constants.someOthreKey}`;
console.log(string); // or use the string in some other way

See the problem? This prints out Welcome to undefined because we typoed someOtherKey. Sometimes it’s convenient that Javascript lets you access arbitrary fields on objects, but often it will cause bugs. This dynamic, flexible typing can be dangerous because Javascript basically lets you do anything. No, seriously:

console.log(undefined + 1);   // NaN
console.log(NaN + 1);         // NaN
console.log(undefined + '1'); // undefined1
console.log(null + 1);        // 1
console.log([] + 1);          // 1
console.log({} + []);         // 0
console.log({} + {});         // [object Object][object Object]
Good old Javascript.

You get the point.

How can we prevent these and other kinds of type-related bugs? How can we stop Javascript from letting us do so much nonsense?

Static Type Checking for Javascript

flow homepage
The Flow homepage.

One good solution is to use a static type checker like Flow or TypeScript. These let you add types to your code and then remove them during build (compile) time to leave normal Javascript code. Here’s a simple example of Flow trivially fixing our earlier code:

// @flow
const addOne = (x: number) => x + 1;const object = { value: '2' };

console.log(addOne(object.value));

All we had to do was add the number type for Flow to set us straight:

flow example 1

Similarly, Flow completely eliminates “I typoed the object field name” bugs:

// @flow
const Constants = Object.freeze({
  someKey: 'value',
  someOtherKey: 'otherValue',
});

const string = `Welcome to ${Constants.someOthreKey}`;

flow example 2

Seems useful, doesn’t it? These are only two of many cases where Flow could help prevent bugs.

To put the cherry on top, Flow is also super easy to setup in an existing codebase. I’ll prove it to you: we’ll integrate Flow with my open-source example-io-game codebase (from my building an .io web game series) right now!

Setting up Flow for a Website

If you want to follow along, download the code first. If you’re curious, you can also check out a live demo of the code.

What exactly this codebase does is actually pretty irrelevant: all you need to know is that it uses Webpack and Babel 7 for builds.

To start, we’ll install some dependencies we need:

$ npm install --save-dev flow-bin @babel/preset-flow

Then, we need to make sure Babel strips out our Flow typings at build time. This is super easy:

.babelrc
{
  "presets": ["@babel/preset-flow"]
}
We create a .babelrc since we don't already have one.

Let’s add a flow script to our package.json for convenience:

package.json
{
  // ...
  "scripts": {
    "flow": "flow",
    // ...
  }
}

Finally, let’s initialize Flow by running

$ npm run flow init

This creates an empty .flowconfig file for us. That’s it! Now we can use Flow!

$ npm run flow
No errors!

Running Flow doesn’t actually do anything yet because we haven’t activated it for any files. We can enable Flow on a file-by-file basis by adding a // @flow comment at the top of any file. Let’s try it for the src/client/assets.js file:

assets.js
// @flowconst ASSET_NAMES = [
  'ship.svg',
  'bullet.svg',
];

const assets = {};

// More code
// ...

Running npm run flow again gives us one error:

flow integration 1

We have to declare the type of any function arguments. This is an easy fix:

assets.js
// @flow
const ASSET_NAMES = [
  'ship.svg',
  'bullet.svg',
];

// ...

export const getAsset = (assetName: string) => assets[assetName];

Now we’re good:

$ npm run flow
No errors!

We can now incrementally enable Flow for the rest of our client JS files. We’ve fully integrated Flow!

The Bad

We’ve seen some of The Good. What about The Bad?

In my opinion, there are 2 primary kinds of drawbacks to using something like Flow: I’ll refer to them broadly as investment and pace.

The Bad #1: Investment

In short: learning and using Flow can be a big investment. This is especially true if you’re working with other developers: are you going to make everyone learn and use Flow? As with any new technology, there’s work you have to put in upfront in order to reap rewards down the line.

Often times, this investment is worth it. But, depending on the project, you might be better of skipping static typing.

The Bad #2: Pace

Using Flow will almost certainly slow down how fast you can develop (but maybe that’s a good thing…). Adding types will increase the verbosity of your code and possibly make it a bit harder to read. Plus, sometimes you’ll run into situations where you know what you’re doing is fine but Flow doesn’t believe you, usually because it doesn’t support that specific scenario well. Jumping through a few hoops to write your code the way you want to can be a bit of a nuisance.

Again, often times this pace slowdown is worth the benefits. If you’re working on a large project that’s built to last, using Flow is almost certainly a good idea. On the other hand, if you’re just throwing together a small weekend project, it’s probably safe to skip static typing.

To Recap…

Using a static type checker for Javascript can greatly improve the safety of your code, but it does comes with drawbacks you should be thinking about.

Some Flow resources:

Did you know that you can also use Flow with Node.js to type-check your backend Javascript code? If you’re interested, read more about how to use Flow with a Node.js project.

I write about ML, Web Dev, and more. Subscribe to get new posts by email!



This blog is open-source on Github.

At least this isn't a full screen popup

That would be more annoying. Anyways, consider subscribing to my newsletter to get new posts by email! I write about ML, Web Dev, and more.