At work we have a fairly large React Codebase that is about a year old and has been typed with Flow from day one. I always wanted type support in Javascript and at that time, Typescript was out of the question since it didn’t support many of the features I take for granted in my Javascript work (ES6/7 features, most importantly array and object destructuring). So we went with Flow, under the assumption that since both Flow and React are Facebook projects, they would work nicely together.
A year later, we are in the process of completely replacing Flow with Typescript. Over the last year, Typescript has added all the features I was missing, while Flow started to show serious problems. I want to list some of the individual points that made us switch and then give some advice for projects attempting the same.
Unhelpful error messages
Although facebook has made some attempts to improve the errors in Flow, the current state of things is that errors usually show where they were detected, not were they’re caused. The most dreaded we encountered time and time again is Incompatible instantiation for T
, T
being a type variable.
Flow doesn’t tell you what the incompatible instantiation is or where it thinks this instance occurs, leaving you to guess and attempt to tighten down things by adding more explicit annotations everywhere. We never got this to work and usually resorted to suppressing the error.
Compare this to Typescript, we had the same problem appear, but Typescript tells you exactly where an incompatible Type flows in and tells you precisely which part of the type is incompatible, enumerating all possible alternatives. This is enough to quickly find the source of these problems.
There are some issues that are too tricky for Typescript to solve. To circumvent these, the way to go seems to be to cast some expression to any
and then to the type you know it has. You need to be explicit here but you can be sure that whatever problem exists, doesn’t propagate
Community and relationship to React
My original assumption was that Flow would be the ideal choice for a React project since both are developed at facebook. After a year I don’t have the feeling that the React and Flow teams talk much to one another.
There are certain programming patterns in React that are a little off, compared to regular Javascript. Flow and React coming from the same shop made me assume that Flow would provide better solutions to work with these patterns, yet they work just as well or bad as the ones found in Typescript.
I take this as a sign that Facebook isn’t as invested in Flow as it should be. Microsoft on the other hand has given Typescript a ton of attention. Progress is fast, there are open discussions with the community about new features and they even built their own editor for Typescript (more on that in the next section).
I don’t have concrete numbers, but it feels like there are way more developers out there using Typescript than there are using Flow. This results in a much better selection of exiting typings and more places where I can go to look for help and answers. To be fair, recently (February 2017), Flow seems to be gaining a little more traction, but it’s still not quite there yet.
Bad Tooling
Getting Flow to run is not easy. For the Mac, luckily there are prebuilt binaries available through homebrew but if you need to compile it yourself, you need an OCaml development stack set up. The flow binary starts a server that monitors your files and can be queried for errors or type information.
These queries can be sent using the flow binary against a running server. Integration into different editors and IDEs is done through third party plugins. To be fair, I’ve only tried Vim, one of my coworkers had success with a plugin for Atom. But developing Flow never ever felt like developing in a typed language. It is noticable that Flow is a type system that has been bolted onto an untyped language.
The flow binary provides some helpers for analyzing your code. There is the coverage
command that tells you how many of the expressions in a file Flow knows about. It comes with a --debug
flag whose output looks like this:
$ flow coverage --debug src/index.js
5:6,5:16: (true)
5:19,5:27: (true)
5:19,5:49: (true)
5:43,5:48: (true)
7:0,7:6: (false)
7:0,7:26: (false)
7:7,7:13: (false)
Very helpful. You can also tell it print your file, coloring some expressions in red. Funny enough, using type-at-pos
, I’ve seen both red and normal text whose type Flow thought was (unkown)
.
Typescript on the other hand is its own language, with its own spec. It happens to translate straight to Javascript, but the presence of a spec makes it feel much more stable. Typescript doesn’t have to accomodate untyped Code (although it lets you do so), and can operate in a much stricter, well-defined environment.
The biggest advantage when working with Typescript in Terms of tooling though is Visual Studio Code, an editor written in Electron, like Github’s Atom. Editing Typescript with VSCode gives you all the power and convenience of a big IDE. Jumping to definitions, autocompletion, JSDoc tooltips, refactoring commands, inline rendering of type errors are things I don’t want to miss again. The increase in productivity these features provide is huge!
No confidence
Even though Flow supposedly makes a distinction between strict
and loose
mode, I found that strict does not, in any way prevent untyped code from passing checks. To the contrary, it feels very much like untyped code is contagious. Everything it touches becomes untyped too. Files that were once typed, can silently become untyped and stop producing errors. And you’ll never know about it unless you use type-at-pos
on the commandline to manually sample expressions.
While all the previous points were annoying but acceptable, this lack of reliability finally caused us to abandon Flow. The entire purpose of a typechecker is to give me confidence that certain problems are absent from my codebase. Flow will happily proclaim No errors!
, while I stare at code that is obviously wrong. I am adding type annotations that make my code hard to read left and right, only to have Flow ignore them.
I’d rather have no types at all than this false sense of safety. We were ready to abandon types in Javascript altogether, but around that time Typescript 2 was released, so we played around with it a bit and decided to make the switch.
Tips for switching
All the work that went into typing the project with Flow did not go to waste. Semantically and syntactically, Flow and Typescript are very, very similar. Writing our code in a way that worked well with Flow meant it also worked well with Typescript. The translation consisted mostly of
- Rename the file to
.ts
- Remove the
// @flow
header - Make slight syntax adjustments (imports, interfaces)
- Fix the type errors that Flow never managed to discover
We are using webpack and were writing our files using ES6 features (through Babel-Loader) and ES6 modules (through Webpack2). Nothing that’s not supported by Typescript. The change in setup then was:
- Add
.ts
and.tsx
extensions to webpacks resolver configuration, before.js
and.jsx
- Add the
awesome-typescript-loader
- Create a
tsconfig.json
with--strictNullChecks
and--noImplicitAny
enabled. If you set your target for typescript toes5
, set thelib
flag to["dom", "es5", "es2015", "es2016", "es2017"]
to prevent complaints about missing definitions for stuff likePromise