Welcome to the third post in my stack improvements series! This time we’ll be talking about Flow, a static analysis tool which can improve your Javascript development no matter your choice of framework or platform.
I was introduced to Javadoc in college, and it seemed like a reasonable idea. You could generate a nicely-structured site with all sorts of program metadata. All you needed was some simple structured comments. JSDoc is the same thing for the Javascript ecosystem. It’s very well-supported - you can even turn on an ESLint rule which requires JSDoc to be present and comprehensive.
But it was at Microsoft where I saw the potential downsides of this kind of documentation:
”GetType - Gets the Type of the current instance.” (MSDN Library)
So much wasted text. So often I would consult the MSDN documentation only after something was unintuitive, expecting to find a note to that effect. But I was almost always disappointed. It was shades away from insulting, especially after spending all that time and disk space to install the entire MSDN Library!
You see why open source is so interesting to me - when the docs are lacking, I just look at the code!
Really, this is the danger of asking people to document everything in a very structured way. You might get compliance, but it might end up as more of a form of protest. Structured but meaningless.
As I wrote more production code, another problem became clear: documentation which might have started out with good intentions rapidly gets out of date.
The decay over time has a couple standard reasons:
Slowly but surely, code changes accumulate without the associated documentation changes.
At this point the documentation is actively harmful for a developer new to the codebase. At best, sprinkling doubt over hard-won conclusions made from reading the code itself. At worst, causing bugs when developers don’t take the time to double-check what they read in the documentation.
What’s the goal, here, really? Why is documentation useful at all? Well, variable and function names aren’t everything. Sometimes you do need to talk about the why of a function, what it’s trying to achieve, why it exists in the first place. Or perhaps its history of attempted optimizations.
And in a language like Javascript, without stated types, documentation can bridge the gap: specifying a function’s return type and the expected types for each parameter.
Okay, so no tool will ever be able to tell whether developers have included a high-quality why comment. But we can make some progress on the types.
The key with Flow is that, unlike JSDoc, the types specified aren’t just for the next developer who comes along. They are consumed by the tool, and are used to help ensure your numbers aren’t being used like strings - unless you really mean it.
The first step towards getting started with Flow is installing the tool itself. If you’re on OSX, I’d recommend using Homebrew to install it globally, for reasons we’ll discuss later. Otherwise, the flow-bin
node module will work:
brew install flow
# or
npm install --save-dev flow-bin
Next, choose a project to experiment with. I jumped in with a 6000-line Node.js/React.js personal project.
Because the flow
command has a bit of an unusual return value scheme, the easiest way to run it is via a package.json
script:
{
"scripts": {
"flow": "flow; test $? -eq 0 -o $? -eq 2"
}
}
Now you’re ready to tell flow
which parts of your project it should look at. Enable analysis of a given Javascript file with this comment at the top of the file:
// @flow
Now you can see if Flow finds anything with this most minimal setup:
$ npm run flow
> application@0.0.1 flow /Users/username/application
> flow; test $? -eq 0 -o $? -eq 2
Launching Flow server for /Users/username/application
Spawned flow server (pid=25082)
Logs will go to /private/tmp/flow/zSUserszSusernamezSapplication.log
No errors!
This first time you run flow
in a given session, you’ll note that it spins up a server. This is the key optimization which ensures that Flow the fastest Javascript static analysis tool you’ve ever used.
The first thing you should know is that once you start adding type annotations to your code, it won’t run anymore. You’re no longer writing Javascript that runs natively in the browser or Node.js. You’ll need to use Babel to clean up your code before it runs. The good news is that if you’re using babel-react-preset
then your setup already removes type annotations with transform-flow-strip-types
. If not, time to add this transform to your setup.
Next, you probably didn’t get a clean “No errors!” run like my example above. It’s time to talk about Flow configuration.
If you’ve got a good number of node modules installed, you might have seen some errors coming from your node_modules
directory. My first .flowconfig
entries were to remove a couple malformed JSON test files put on disk by node modules. Here’s my ignore
section:
[ignore]
# these files don't parse!
<PROJECT_ROOT>/node_modules/config-chain/test/broken.json
<PROJECT_ROOT>/node_modules/conventional-changelog-core/test/fixtures/_malformation.json
<PROJECT_ROOT>/node_modules/findup/test/fixture/config.json
<PROJECT_ROOT>/node_modules/findup/test/fixture/f/e/d/c/b/a/top.json
<PROJECT_ROOT>/node_modules/findup/test/fixture/f/e/d/c/config.json
With that out of the way, you might next see some ‘Required module not found’ errors. I know I did. Once more, I pay for my absolute paths. Instead of providing additional search paths, I found that I needed to translate source paths to target paths. With module.name_mapper
, you specify a regular expression and a replacement value using \1
-style captures. Here I allow for absolute references to my src
and scripts
directories, as well as package.json
in the root:
[options]
# needed for absolute paths
module.name_mapper='^src\(.*\)$' -> '<PROJECT_ROOT>/src\1'
module.name_mapper='^scripts\(.*\)$' -> '<PROJECT_ROOT>/scripts\1'
module.name_mapper='^package.json$' -> '<PROJECT_ROOT>/package.json'
It’s great to get everything working from the command line, but we can do better. Let’s make things more a little more productive for ourselves.
First, let’s get those errors in the editor! I use SublimeText, so I installed FlowIDE via Package Control. It’s a great little plugin! It shows Flow-produced errors right inline, along with autocomplete taken from the Flow server running in the background. And it updates surprisingly quickly after you change the code!
“Using
flow-bin
’s binary will slow down editing features because it is wrapped in a Node script and starts an interpreter on each run.” (FlowIDE docs)
Sadly, if you’re using flow-bin
instead of a globally-installed flow
binary, your FlowIDE experience will be slower. Don’t say I didn’t warn you!
Next, you probably still want to be able to lint your code. Because the default ESLint parser chokes on type annotations, you’ll need to move to babel-eslint
as your parser. This one was easy; I was already using babel-eslint
for class properties support.
But there’s more! It’s important to establish a consistent style, and that includes both type annotation and full type declarations. It’s time to install eslint-plugin-flowtype
. But be sure to start with this configuration, or you’ll immediately have hundreds and hundreds of errors:
module.exports = {
settings: {
flowtype: {
onlyFilesWithFlowAnnotation: true,
},
},
}
With this option enabled, only files which already have the file-level // @flow
annotation will be processed by eslint-plugin-flowtype
.
You may be wondering how to configure the large set of rules provided by eslint-plugin-flowtype
. Here’s what I used when starting out in my project. As usual, I kept rules enabled unless I had good reason for a change:
{
// not compatible with Flow typecasts
'no-extra-parens': 'off',
'flowtype/boolean-style': 'error',
'flowtype/define-flow-type': 'error',
'flowtype/delimiter-dangle': ['error', 'always-multiline'],
'flowtype/generic-spacing': 'error',
'flowtype/no-dupe-keys': 'error',
// over time, you can transition from Object to named types
'flowtype/no-weak-types': ['error', {
any: true,
Function: true,
Object: false,
}],
'flowtype/object-type-delimiter': 'error',
'flowtype/require-parameter-type': ['error', {
excludeArrowFunctions: true,
}],
'flowtype/require-return-type': ['error', 'always', {
excludeArrowFunctions: true,
}],
'flowtype/require-valid-file-annotation': ['error', 'always', {
annotationStyle: 'line',
}],
'flowtype/semi': 'error',
// the ability to organize conceptually is important
'flowtype/sort-keys': 'off',
'flowtype/space-after-type-colon': 'error',
'flowtype/space-before-generic-bracket': 'error',
'flowtype/space-before-type-colon': 'error',
'flowtype/type-id-match': 'error',
'flowtype/union-intersection-spacing': 'error',
'flowtype/use-flow-type': 'error',
// deprecated; warns on bad syntax accepted by babel-eslint
'flowtype/valid-syntax': 'error',
}
At a high level, this will force you to annotate all (non-arrow) function parameters and return values. But you can’t use any
, since that’s the same as no annotation. And, as you might expect, its whitespace rules match my general ESLint config.
Now, it’s finally time to start converting files! I went one file at a time in my 6000-line project, top to bottom. Here are some of the key code changes I made:
You can tell Flow that a given variable or parameter can be either a type or null with the question mark: name: ?string
. If you do this, you’ll get an error if you try to do something with the string (like name.length
) before checking against null:
“Property cannot be accessed on possibly null value”
Once you have a guard in place (like if (name)
), Flow will stop complaining.
In Flow, you can specify that a given variable or parameter can be one of a few types, like options: string | string[]
for either a string or an array of strings. Once you use this syntax, Flow will not let you treat that variable like either of those types until you have checked the type.
I was using lodash
to check types before, but Flow doesn’t understand those calls. For example, I needed to convert _.isString(options)
to typeof options === 'string'
. And _.isArray(options)
needed to be Array.isArray(options)
.
propTypes
are a debugging tool to help you ensure that your React components are being used the way they are meant to be. Warnings fire at runtime in development mode when props are missing or the wrong type.
With Flow, we can take this experience into the editor. First, specify the type of a React component’s props
as an object with keys of certain types. When you use that component in JSX, Flow will analyze the values provided as props, and warn accordingly! It was surprising and satisfying to be told that I needed to change some of my tests to pass the right props!
Sometimes you’re doing weird things. It’s not that you can’t express it in Flow’s type system, it’s just that doing so isn’t worthwhile. For example, I was adding random keys to an array - the union type required would have needed typecasts or code changes across the project.
You can create an escape valve with this entry in your .flowconfig
:
[options]
suppress_comment=^ flow-disable-next-line
Like ESLint’s escape valves, you can put it in a comment on the line before any offending code:
// flow-disable-next-line
x.doBadThings();
Once I had my own project’s code converted, it was clear that I could get a whole lot more value out of Flow if I had type information for my dependencies. It was time for flow-typed
.
With a couple commands I had definitions for eight node modules, and placeholders for the rest:
npm install -g flow-typed
flow-typed install
To get Flow to pick them up, my .flowconfig
needed another entry:
[libs]
<PROJECT_ROOT>/flow-typed/
Just like that, I had a whole lot more value out of my Flow runs. I did discover a couple issues with the definitions I had downloaded, but the fixes were easy and I submitted a quick pull request to share the love. And yes, I did end up deleting all of the placeholders.
flow-typed
isn’t as comprehensive as DefinitelyTyped, the TypeScript ecosystem’s type repository. But it will hopefully get there. Alternately, it’s a growing trend for libraries to ship their own definitions out of the gate. We’ll see!
With Flow, we get up-to-date type documentation because we get extra value from the tool if we give it the information it needs to analyze the code. Unlike JSDoc, it doesn’t feel quite so pointless. It fits right into that ‘make it worthwhile’ systems lesson I learned at Microsoft. And when we do start to get lazy, the tool will let us know if the type doesn’t match up with how we’re using it.
Maybe now we’ll have a little bit of extra time to write up that why documentation!
With all the election-related turmoil this year, I thought I would share a collection of books I found useful for helping me make sense of things. The last set of books I shared were about more... Read more »
I’ve been told that I’m a very productive developer. And it’s not magic! Welcome to number three of my developer productivity tips: Be a scientist. Scientists are curious, always learning, creative... Read more »