Exploring Flow, Facebook's Type Checker for JavaScript

At September's @Scale conference, Facebook introduced Flow, a static type checker for JavaScript.

The stated goal of Flow is to "find errors in JavaScript code with little programmer effort". To paraphrase project lead Avik Chaudhuri: Flow aims to enforce the benefits of a type system while maintaing the "feel" of JavaScript.

Avik promised that Flow would be open-sourced by the end of the year and, true to his word, the project appeared on GitHub a few months later.

I wanted to give Flow a whirl, so I decided to integrate it into a small project to which I've contributed: RCSS. You can find my Flow-annotated fork on GitHub.

Working with Flow was, overall, a good experience. The benefits of Flow are immediately evident and integration isn't particularly difficult. However, the project feels raw and there's a lot of room for improvement—it's a work-in-progress, and the docs admit as much. Given time, I'm confident that it will become even easier to use and even more useful.

This post will be part-commentary, part-resource. My goal is to give you, the reader, a better understanding of how to integrate Flow into your projects and avoid any undocumented pain points.

If you have specific questions about interfaces, transforms, object annotations, or globals, feel free to skip ahead.

My Playground

Briefly: RCSS is an experiment in writing CSS with JavaScript, similar to how one might replace HTML with JSX. It's intended to be used with React or another front-end framework.

The RCSS core comes in at around 200 lines of code. It's very small (which is, of course, a good thing). In the context of this experiment, it means that my observations mostly relate to surface-level usage and initial setup of Flow—I tried to hit most of the features, but I certainly can't claim to have exercised Flow's most powerful muscles.

With that covered, I'll start by discussing Flow's server model before getting into some of the intricacies of Flow interfaces.

Server Architecture

Flow is centered around a server architecture, which allows for online type checking. So, to start, Flow scans your entire project—or, at least, any files marked with the pragma, as Flow's type checking is opt-in. When you later change a file, Flow can intelligently type check the paths in the code that were affected by this change, rather than rescanning the entire project. This leads to big performance gains.

To use Flow, you first spin up the Flow server with flow start and then type check your code with flow. Pretty simple.

Even for a tiny project like RCSS, my (slow) machine takes over a minute to kick off the Flow server. But this time is made up in the incremental type checking: once the server is running, the flow command runs instantaneously.

I really, really appreciated the ease that came with running Flow—the feedback loop is incredibly tight. This was one of the major selling points to me coming in to the project and the hype was validated.

Interfaces

Flow allows you to provide a folder of interface files to be used during type checking, identified with:

flow start --lib path/to/interfaces/

Any file in path/to/interfaces will be included as an interface, as long as it ends in .js. The specific naming scheme is irrelevant.

Interfaces should be filled with declarations and are primarily intended for annotating third-party code. The declarations you include in an interface will be global to the type checker.

As an example, say I installed the foo-bar module from npm. This module exports two properties, both functions: reverse, which reverses a string and square, which squares a number. Then, I could add the following declaration to my interface file:

declare module 'foo-bar' {
  declare function reverse(s: string): string;
  declare function square(x: number): number;
}

Notice that I'm able to identify the module by its require-path, which is super elegant.

However, there were a bunch of oddities that I encountered with interfaces that made them pretty hard to use:

  1. Interfaces fail silently (#39). If you have a syntax error in your interface file, Flow will simply ignore it and provide no indication of doing so. This is really, really annoying, especially when paired with the next point.
  2. Interfaces do not auto-reload (#87). In effect, if you change or add an interface, the only way to reflect that change in the type system is by running:

    flow stop
    flow start --lib path/to/interfaces/
    

    As I mentioned before, this can be a lengthy process that really slows down your speed of development.

  3. Module interfaces don't support non-object exports (#73). In my initial example, notice that I annotated the object properties exported by the module. If my module, for example, exported just a function, there wouldn't be a good way to annotate it.

    Admittedly, there are a few alternatives. For one, I could annotate the module on import. Assume now that I have a module reverse that just exports the string reversal function. On import, I could do:

    var reverse: (s:string) => string = require('reverse');
    

    The downside: I need to include this annotation whenever I import the module.

  4. Interfaces are shallow (#93). If you specify an interface directory, only files in the immediate directory will be scanned and treated as interfaces. (There's an open pull request to fix this.)

I'm not trying to be overly critical of Flow—all of these issues are fixable and there are indications that they will be fixed (as evidenced by the GitHub issues referenced above). But for those experimenting with Flow in the meantime, it's very, very good to be aware of them.

Globals

There isn't a great way to deal with globals in Flow. Here, I'm specifically talking about anything that would be on window in the browser or global in Node.

For example, let's say you're the type of rebellious JavaScripter that uses the Function function, like this:

var square = Function('function(x) { return x * x; }');

Flow will throw an error here, claiming that it can't find a callable signature for Function.

If you want your code to run in both Node and the browser, you obviously can't hardcode global or window, respectively. Instead, the Flow docs suggest you do this:

var root: any = this;
var square = root.Function('function(x) { return x * x; }');

I found this to be a reasonable but somewhat unsatisfying answer. It'd be a little annoying to have this root pattern scattered throughout my codebase. But I can't think of a much better solution.

For example, one alternative would be to provide a declaration for Function in the same file it's used, e.g.:

declare var Function: any;
var square = Function('function(x) { return x * x; }');

I prefer the former. (Surprisingly, putting this declaration in my interface file didn't work.)

Transforms

Per the docs, you need to use Facebook's JSX transformer to strip away Flow's type annotations.

Specifically, assuming you have react-tools installed globally, you strip types away with:

jsx --strip-types index.js

The docs provide this more useful and more realistic example, which includes Harmony transpilation and watching:

jsx --strip-types --harmony --watch src/ build/

I wanted to use this transform with browserify. Luckily, the --strip-types option is already built into reactify, the JSX browserify transform, so you can run:

browserify -t [reactify --strip-types] index.js

There's also the flow-typestrip package, a standlone Flow annotation transformer, which works as a drop-in replacement:

browserify -t flow-typestrip index.js

This transpilation step, while somewhat inconvenient, is now the status quo for a lot of JS tooling.

Commented Annotations: The Future?

That said, there's been some interesting discussion on GitHub about removing this transpilation step for Flow through the use of formatted comments.

Right now, Flow provides a means of generating annotations from docblock comments. For example, if you run flow port on the following:

/**
  @param {number} x
  @return {number}
 */
function square(x) {
  return x * x;
}

square(5);

It produces a type-annotated file:

/**
  @param {number} x
  @return {number}
 */
function square(x: number): number {
  return x * x;
}

square(5);

While the core team has noted that this should be considered experimental, some commenters have suggested performing this transformation in-place and using the output annotations for type checking. There are good ideas in that thread and I'm excited to see what the Flow team produces.

Objects

In Flow, there are a few ways to annotate an object. The most basic, as you might guess, is to add an annotation to the object literal at the time of creation:

var foo: { x: number; y: string } = {
  x: 5,
  y: 'hello'
};

Any property included in the annotation must be included in the object at time of creation, so we get an error here:

var foo: { x: number; y: string } = {
  x: 5,
};
// Property not found in object literal

If you want to reuse an object's type annotation, you can implement it as a type alias, which looks like this:

type FooType = { x: number; y: string };
var foo: FooType = {
  x: 5,
  y: 'hello'
};

Both of these approaches catch reads from and writes to undefined properties, like this:

type FooType = { x: number; y: string };
var foo: FooType = {
  x: 5,
  y: 'hello'
};
foo.z = 3;
// Property not found in object type

Types can also be inferred from existing objects using the typeof operator. This is a really, really cool feature. For example, Flow would catch an error here, as the properties x and y have incorrect values in the second constructor:

type FooType = { x: number; y: string };
var foo: FooType = {
  x: 5,
  y: 'hello'
};
var bar: typeof foo = {
  x: 'hello',
  y: 5
};

Unannotated Objects

Without annotations, the typing on objects becomes pretty loose, as you'd expect—it's hard to enforce type safety on an object that you know nothing about.

So, for example, if we omitted FooType in the previous snippets, things change a little bit. Flow will enforce type consistency on the properties defined at the time of object creation, so we get an error here:

var foo = {
  x: 5,
  y: 'hello'
};
foo.x = 'world';
// String is incompatible with number

However, Flow has trouble tracking properties that are added to the object after creation. The docs make some guarantees about the extent of the type checking for such a scenario, but behavior is hard to predict. For example, we (rightfully) get an error here:

var foo = {
  x: 5,
  y: 'hello'
};
foo.z = 3;
var s: string = foo.z;
// Number is incompatible with string

We also get an error here, but for a surprising reason:

var foo = {
  x: 5,
  y: 'hello'
};
foo.z = 3;
foo.z = 'hello';
var s: string = foo.z;
// Number is incompatible with string

Note that the error here comes on the final line, so Flow lets us overwrite foo.z with a string, but later enforces that its type should be number. Odd. I think this is a bug, but the general point is that I wouldn't rely on unannotated objects.

Maps

Tucked away in the Flow docs is an interesting note about treating objects as key-value maps.

Specifically, you can provide a blanket type for the keys and values of an object or type alias, e.g., if you wanted an object that mapped from strings to numbers:

var foo: { [key:string]: number } = {
  'hello': 0,
  'world': 1
};

This is pretty useful: it feels like a lot of type safety for very little effort and allows for a fair amount of flexibility. For example, we can add new properties to foo after creation and get a good level of enforcement:

var foo: { [key:string]: number } = {
  'hello': 0,
  'world': 1
};
foo.goodbye = 2; // success!
foo.seeya = 'bump'; // error!
// String is incompatible with number

There are a few 'gotchas' here that aren't mentioned in the docs:

  1. The word key in the above snippet could really be replaced with any other word. There's nothing particularly special about it; it just acts as a placeholder. So this is just as good:

    var foo: { [abcdefg:string]: number } = {
      ...
    };
    
  2. If you're using anything other than strings as your keys, your syntactic choices become very limited. For one thing, in general, non-string-literal keys aren't supported by Flow. So if you want to use numbers as your keys, this won't work:

    var foo: { [key:number]: string } = {
      0: 'hello'
    };
    // Non-string literal property keys not supported
    

    Instead, you need to initialize an empty object and use bracket-syntax for assignment:

    var foo: { [key:number]: string } = {};
    foo[0] = 'hello';
    
  3. Flow lets you include string-literal keys in the object constructor even if you include one of these map annotations. However, reading from or writing to these keys in the future always throws an error:

    var foo: { [key:number]: string } = {
        hello: true
    };
    foo.hello;
    // Number is incompatible with string
    

    This holds true even if you include the property in the type definition:

    var foo: { hello: boolean; [key:number]: string } = {
        hello: true
    };
    foo.hello;
    // Number is incompatible with string
    

    It's unclear to me what the expected behavior is here. Mixing these annotations could be a means of marking some keys as required, but if that's the intention, it would only work when both the property-specific keys and map keys are strings: if the property-specific key isn't a string, Flow won't accept it at time of construction, and if the map keys aren't the same as the property-specific keys, you'll get an error on read or write.

    (I've documented this behavior on GitHub.)

Type Reuse

Lastly, I want to talk a bit about type reuse (across files) in Flow.

In RCSS, we have an object that pops up in a few different modules. It stores both a CSS class name (as a string) and a style object (i.e., a map from CSS properties to strings or numbers). You could implement a type alias for this object like so:

type StyleObjType = {
  className: string,
  style: { [key:string]: number | string }
};

Note that I'm using a union type here.

The fact that this object is used in multiple modules was problematic. I had a few choices:

  1. Redefine the type alias in every file.
  2. Define the type alias in an interface file, common to all files type checked by Flow.
  3. Export an object with this type from some other file and use typeof on that export. For example, I could create style-obj-type.js with:

    /* @flow */
    
    type StyleObjType = {
      className: string;
      style: { [key:string]: number | string }
    };
    
    var exemplarObject: StyleObjType = {
      className: '',
      style: {}
    };
    
    module.exports = exemplarObject;
    

    Then, elsewhere, I could reuse the type with:

    var exemplarObject = require('./style-obj-type.js');
    var styleObj: typeof exemplarObject = {
      ...
    };
    

Clearly, each option is awkward in its own way. I went with option #2, but was sort of unhappy to have a single interface file with global type aliases. In a larger project, I could see this becoming inconvenient or difficult to maintain.

In general, it'd be nice if there was a better mechanism for sharing these kinds of type aliases across files. Option #3 would be neat if I could export the alias, rather than this dummy object satisfying it.

Things improve when you use ES6 Classes, as the classes can be exported and their types reused with ease.

In Conclusion

I had a positive experience with Flow and look forward to using it again. Admittedly, though, the project felt raw, with a few 'gotchas' scattered throughout the codebase.

Much of this could be solved with better documentation and, with any luck, I'll be sending over some pull requests in the near future (one down!).

Even better, many of the issues I described here have already been noted by the community, which seems to be growing and going strong. If Flow is run half as well as React, I'm sure it will continue to prosper as an open-source project.

Appendix: Other Cool Features

If you're interested, here are some other cool features of Flow that I didn't spend much time on in this post:

Thanks to Shubhro Saha for his feedback on a draft of this post.

Posted on December 12, 2014.