Going With the Flow

Published by Karol Majta on 16th Apr 2019

Tiny Endian

When it comes to typed languages that target JavaScript runtimes there is plenty to choose from. From some more extravagant ones like Elm, PureScript or Reason, through a few being somewhere in the middle (Dart), to ones fully embraced by the community -- yes I am pointing at TypeScript.

And then, there's Facebook's Flow, "just" a type checker. Here are four reasons why I got hung on it.

1. Flow does one thing and does it well

On it's webpage Flow's authors dub it a type checking tool, a small part that for most languages is often hidden in the broader term "compiler". What are the implications?

When using flow, you combine it with plain old JavaScript with the de-facto standard, configurable "compiler" Babel and fairly standard package management tools (npm or yarn).

Flow being focused solely on type checking there really isn't much to grasp besides the type system itself.

Adding Flow to your toolchain boils down to adding @babel/preset-flow preset to your Babel config, or you can choose not to make any changes if you decide to use the comment based syntax.

Changes to source code are minimal and not required, as by default Flow will only check files that start with // @flow pragma.

Because of reasonable default settings (ignoring node_modules etc.) there is a good chance that your project already is Flow compliant! Give it a try and just run npx run in your project's root directory to find out.

Compared to TypeScript this feels refreshingly light. Yes, the complexity of using tsc is now neatly hidden behind a dedicated preset, but starting with TypeScript still feels a lot more challenging than with Flow.

2. Flow gets along with your old friends

Be it your old codebase or your old coworkers, Flow is designed to not get in the way. In one of the teams I worked with we successfully added Flow to JS codebase that was few hundred thousand lines. And added (which is slightly different than migrated to) is the key word here.

At some point we realised that we desperately need a type system in some parts of the project. We were also certain that we didn't need it everywhere. Discussions bursted. Should we use TypeScript? Should we migrate all or parts of our codebase to TypeScript? Do we really want to maintain an extra build-tool in an already quite complex process? As probably imagine getting an answer that would satisfy everyone on the team would be impossible.

Instead of trying to please everyone we quietly gave Flow a spin. We started with the amazingly useful comment based syntax, and to make the transition even smoother we kept the type definitions in separate files. An example of such file would look like this:

/* MathModule.types.js */
// @flow
/*::
export interface IMathModule {
  add: (x: number, y: number) => number;
  subtract: (x: number, y: number) => number;
  multiply: (x: number, y: number) => number;
  divide: (x: number, y: number) => number;
}
*/

And then, in a file containing actual implementation changes could be kept to the minimum

/* MathModule.js */
// @flow
/*::
import type { IMathModule } from './auth.model.types.js';
*/

const MathModule /*: IMathModule */ = {
  add: (x, y) => x + y;
  subtract: (x, y) => x - y;
  multiply: (x, y) => x * y;
  divide: (x, y) => x / y;
}

export default MathModule;

This example, while a bit artificial shows that there is really little change required in the actual implementation file, and during further work on this code, maintenance of types happens mostly in MathModule.types.js so people who are not yet sold on the idea of typing can only skim through it during code reviews etc.

We continued with thin unobtrusive approach without adding any extra dependencies to the project. We even made the flow step optional during build time (required before commit however) so it didn't get in the way of people who didn't give much care about types. Soon enough we had the "rough" part of our code typed and throwing less runtime errors.

3. Flow never slows you down

Another great thing about Flow is the fact that it is absolutely optional to run flow. The type checking phase (running flow) is completely separate from removing Flow annotations from source code (done by @babel/preset-flow) and so if you want to try running code that does not conform to the type system you are free to do so.

Lots of people will tell you that suppressing type errors is a bad thing<sup>TM</sup> and TypeScript makes it hard on purpose, but the reality is slightly different...

Let's imagine you're coding a react view, and you just need this red 50 pixel bar to show up, and also this simple toggle showing the comments to start working. You don't care about the types. You care about the UI. You know you might not get every edge case right, but you need to see how this behaves on multiple screens and just get over with it. At this particular moment, null doesn't matter, the layout breaking on a 6 year old Nokia does. This is how people worked with UI and JavaScript for years and this is what they love it for. Come on TypeScript, I don't need to fix your errors right now when I'm in the middle of something important. I'm an adult, I'll ask Flow later.

4. Flow and React play nicely together

This is pretty subjective, but I found working with React and Flow nicer than with React and TypeScript. TypeScript's React support is good, but Flow was build with React as a first class citizen in mind. I never had any trouble finding out how to annotate the most complex of my components.

Flow is great at things that matter most

Flow and TypeScript don't differ that much. In the end you will spend a promile of your time wondering about subtle differences in function covariance and contravariance, and the rest on using lots of string and Array<number>. Most of the time your choice just doesn't matter, because both tools will perform equally good for rudimentary tasks. And since features of the type system don't matter that much, let's focus on things that do:

  • Simplicity of use
  • Possibility to use in legacy codebases
  • Flexibility
  • No Overhead
  • Ease of adoption for people who don't care about types
  • Getting stuff done without too much distraction

I find Flow superior in all six, and I am sad to see projects leaving it (see here) just because TypeScript has bigger user base.