log in

Overcoming a false sense of security when using Flow types

lobste.rs - Wed Jan 13 18:24

by Jared Forsyth

At Khan Academy, we’ve been using Flow ever since we started using React Native in early 2017, and it’s far better than not having types. However, it’s an incomplete type system, and often large portions of code are riddled with any types. This can sometimes lead to a false sense of security, which is then burst when you get a runtime type error and think “wait, I thought flow had my back!” Our solution to this problem is “uncovered expression linting.”

The end result is that, when you are reading over some code, you can be confident that flow is checking it for safety, except where explicitly indicated with “linter disable” comments. This greatly improves our ability to be confident in the soundness of our code, which frees up mental space for all the other parts of building a system.

Type Coverage

Now, flow has built-in “coverage reporting,” where you can see what percentage of your code is covered — that is, verified as being “safe” by the type system. The opposite being code that is exempted from the type system, via any — and anys are infectious if unchecked. Any function that you call that returns an any (either explicitly or implicitly) will result in a “stealth any”: at the callsite there’s nothing to indicate that the value you got back has been exempted from type safety.

Similar to test coverage reporting, you can use this to ensure that your overall “percent type coverage” remains above a certain threshold.

Unfortunately, this kind of metric is nearly useless. One major job of a static type checker is to give you confidence that your code is sound — to free up your mental space from doing the work of type-checking code you’re reading, so that you can focus on other things. But if the code isn’t fully covered — if it’s only 80% covered, for example — and you don’t know when you’re reading some code just which parts aren’t covered, then the benefits of a static type checker are greatly reduced. You’re left with insecurity — knowing that there are places in your code that are much more error-prone, but not knowing where they are.

If you’re using VSCode with flow integration, then you can turn on inline coverage reporting — code that is exempted from the type system gets underlined in a blue wavy line. This is much better than relying on a percentage, but still has two downsides. The first being that it doesn’t help for code reviews on github or your code review platform of choice. When reviewing someone else’s code, knowing what parts of it are “more dangerous” is quite important. The second and more fundamental issue is that there’s no way to indicate that a given expression or block of code is intentionally uncovered, and has been given the scrutiny it deserves.

This is where linting comes in!

The Solution

Instead of a broad per-project or even per-file coverage percentage, we lint against any uncovered expression. For example, the following code would contain 5 linter errors (highlighted and underlined in red, each of them “this expression isn’t covered by flow”):

1| const liveDangerously = (one: any, two): any => {
2|   return one + two;
3| }
4| const andSuch = () => {
5|   const theResult = liveDangerously("a", "b");
6|   return theResult + ":yes"
7| }

Notice how, without this linter, you wouldn’t necessarily know that using theResult was unsafe, because there’s nothing in the source to indicate it.

Of course there are sometimes good reasons to have any typed code. JSON.parse, for example, rightfully has any as the return value. In cases such as these, however, having the linter encourages developers to add type annotations as early as possible, so as to minimize the number of “linter disable” comments that are needed.

We’ve been using flow coverage linting for the past two years, and we love it. We’re currently using a custom in-house linter, but we recently added uncovered expression linting to the “flowtype errors” eslint plugin, which will enable us to get our flowtype coverage errors along with our other eslint reporting.

If this sounds interesting to you, all it takes is npm i -D eslint-plugin-flowtype-errors and adding the following to your eslintrc:

    "plugins": ["flowtype-errors"],
    "rules": {
        "flowtype-errors/uncovered": "error",
        ... etc

And then you get this!