Hardening Javascript Application with Static Type Analysis
You’ve probably seen your share of “Cannot read property of undefined” or “Undefined is not a function” errors in Javascript. We all have. After all, Javascript is a relatively weakly-typed language, so these things are bound to happen. But, there’s a way to avoid annoyances like data not arriving when expected (or at all), property names typos, and the countless other little things that can cause big headaches - it’s called static type analysis.
Meet Flow
Flow is a static type checker for Javascript that analyses code to find weak spots where you’re not treating your data the way you’re supposed to. This includes things like reading from possibly-undefined values, passing wrong types to functions, and more.
It’s actually pretty simple. You just describe your data the way they are supposed to work, and Flow will make sure your code treats them such. Let’s say we have a following data structure and function to read the title of the TV series an episode belongs to:
type EpisodeType = {
title: ?string,
tv_series?: {
title: ?string,
},
};
Without annotations, the function looks like this (and will work most of the time):
const getSeriesTitle = (asset) => {
return asset.tv_series.title;
}
Now, let’s start adding annotations:
const getSeriesTitle = (asset: EpisodeType): ?string => {
return asset.tv_series.title;
}
Flow will immediately complain that you can’t access title because asset.tv_series could be undefined.
Cannot get asset.tv_series.title because property title is missing in undefined [1].
[1] 5│ tv_series?: {
6│ title: ?string,
7│ }
:
21│
22│ const getSeriesTitle = (asset: EpisodeType): ?string => {
23│ return asset.tv_series.title;
24│ }
So you need to fix your code to pass the analysis, like this:
const getSeriesTitle = (asset: EpisodeType): ?string => {
return asset.tv_series && asset.tv_series.title;
};
This function could return string or undefined, so next time you’ll use it, Flow will force you to check for undefined on its return value.
Since your function is now annotated, Flow will serve as a live reference for your APIs and will make sure you always use them correctly, including what the function expects and returns.
I’m in. Where do I begin?
You don’t have to go all nuclear on your code and rewrite everything - you can start adding Flow annotation one file at a time and adopt it at your own pace. There’s a very easy Installation guide that can help you set it up with either babel or a simple CLI tool that will remove the annotation prior to final build.
When you’re done with that, you’re ready to start coding. Helper functions or data processing functions are usually a good start - just add // @flow to the beginning of the file, annotate function arguments and return value, and run flow status. You’ll see right away where you’re using possibly undefined values and related issues. Check the Reference on how to annotate your data. When you’re done with your helper function, you can try to add // @flow to the file where you’re using it to see the fruits of your hard work.
Is it working? I don’t see anything.
It may go without saying, but it’s true - if your project works, you’re probably doing things right most of the time. But it’s always better to be sure. The flow-coverage-report tool will help you with just that. It creates a report on how well your files are covered and marks all the blank spots that Flow can’t see through.
npm install -D flow-coverage-report
Now you just need a little configuration on what files to include (source files), what to ignore (tests), what type of report to generate, number of files concurrently scanned, and coverage threshold for PASS mark.
{
"includeGlob": [
"src/**/*.js"
],
"excludeGlob": [
"src/**/*.spec.js"
],
"type": [
"text",
"html"
],
"concurrentFiles": 4,
"threshold": 50
}
Let’s run the report:
flow-coverage-report --config .flow-coverage.config.json
It will create HTML report in flow-coverage folder in your project:
You can see that the getAssetPageUrl function is red. That’s because it’s not yet annotated, and Flow will treat its return value as any. You’ll probably see a lot of red in the beginning, but it will get better over time. You’ll also probably see in the report that a lot of files have quite high coverage, sometimes 100%, even when they’re not marked with // @flow to be included in the analysis. That’s because Flow infers a lot of types on its own. You should add the comment to these files to get them into the game.
I’m using a lot of 3rd party libraries, what about them?
Flow is being widely adopted in the community, so some of your dependencies already come with Flow annotation. What’s more, Flow can also leverage packages written in Typescript, so chances are, that some part of your stack is already covered. For everything else, there’s a flow-typed project - a collection of high-quality annotations for a lot of popular Javascript libraries that you can add to your project.
npm install -g flow-typed
flow-typed install react-redux@5.0.4
Done. Your react-redux is now annotated.
If you run just flow-typed install, it will download annotations for all your packages in the repository and create blank ones for the rest, but you’ll probably end up with a LOT of errors in your code, so I recommend to go one package at a time.
“I’m using react-proptypes, isn’t that enough?”
Good for you! But you’re just validating props of your components during runtime. It doesn’t cover rest of the code that prepares those props, or possible race conditions when data is not yet ready. You can replace PropTypes with Flow completely, but if you still want to keep the runtime check, there’s a babel-plugin-flow-react-proptypes that will generate React PropTypes from the Flow annotation of your component. That might be useful in case your components will be used in projects that don’t use Flow:
import type { Node } from 'react';
type Props = {
backgroundImage?: string,
backgroundColor: ?string,
textColor: ?string,
children: Node,
};
type Context = {
browserSupport: {
webp: boolean,
},
};
const PageBackground = (props: Props, context: Context) => {
...
}
Functional component annotation
import React, { PureComponent } from 'react';
type Props = {
width: string | number,
height: string | number,
imageUrl: string,
imageSet?: string,
backgroundPosition?: string,
};
type State = {
backgroundImage: ?string,
};
class Image extends PureComponent<Props, State> {
...
}
Class component annotation
You can just add it to your babel configuration and start removing PropTypes definitions whenever you annotate your component with Flow. PropTypes do sort of duplicate the work of Flow, but if your application is not completely covered with Flow, or you’re going to use the components in project without Flow, it might be worth it to keep it. It’s automated anyway, so it doesn’t hurt, does it?
Is the overhead worth it?
Yes. Adding Flow annotation to your project will take some extra time for sure, but it will save you much more. It’s not just about bug-fixing another “Cannot read property of undefined” error. You don’t have to write tons of unit tests and integration tests to cover the cases when you receive invalid data or no data at all (you wouldn’t have thought about every case and combination anyway ;)). You can do a better job with Flow in a fraction of time of regular tests. It doesn’t get easier over time, as your code will be more tied together, but it’s definitely worth it.
follow the article to get updates and comment.