Getting Flow to agree with your types

Photo by Ian Espinosa on Unsplash

Flow is notoriously hard to use. I believe in the importance of strong static type-checkers. I have been using flow for about two years and don’t regret it, but flow requires too much understanding of how it works to make casual people happy using it. Most of the people I worked with, in different companies, on different (react) projects, think that flow is helpful. But they hate it. And they don’t intend to spend, as I did, countless hours following weird stack traces to understand what to do.

By the way, for each of the shown examples, you’ll find a link to, so you can play with the code to better understand why it works this way. And here is a repo with all the examples.

Quick wins

The first problem people encounter is refinement invalidation. The beauty of flow is that it is, most of the time, smart enough to understand the execution flow of your program. If you check that a variable is defined, it’ll know that next line, it will still be defined. At the condition that you didn’t call a function between the check and the use. In JavaScript, function calls are not pure, and can do a lot of unexpected things behind the scenes. And flow is pessimistic about that, considering that a console.log can change the value of the object you’re using. The solution here is usually to move the check closer to the use of the variable. Or use an invariant (more on that later). You’ll also have this problem with closures.

Refinement invalidation through function calls — Flow try

Refinement can be the source of other kind of issues. There are situations where flow cannot refine a variable’s type. It struggles when variables depend on each other, refining one will most of the time not help flow to understand the type of another. Predicate functions (e.g. lodash.isNil), if not specifically typed, won’t work. To make it work, you’ll have to use %check. Another common pain point is array.filter. Apart from filter(Boolean) which will successfully remove falsy values, it won’t help. Usually, array refinement will be easier to do with reduce. Or with a for loop.

When you believe that a type should be valid (a cat is an animal, right?), and flow complains, Flow is usually right there. This notion of type/subtype/supertype is harder than it seems. And often not intuitive. Indeed, when using a type instead of another one, you should just not consider the values of the type (e.g. the properties of an object), but all the operations you can do to both types. You cannot pass an object with a non nullable age to a function taking one with a nullable age. This function could delete the age property, and since we use references in JavaScript, this could lead to a bug.

Here you’ll have to use read-only types (aka covariance), which uses the + sign before the property, or the $ReadOnly<> type around the object.

You need to use read-only properties to pass objects to function accepting nulls — Flow try

This also applies to arrays.

Like objects, don’t forget that arrays are mutable — Flow try

If you need to rely on the non existence of some key, don’t forget the existence of exact types. On a non exact type, Flow will consider that a missing property can still be there, and will ignore your tentative to use it to refine your type.

You’ll need to use exact types when checking that a property doesn’t exist — Flow try

Using more advanced features of flow, such as unions, or composing types will often become necessary, but it’s also source of mistakes. To compose types, you’ll have two possibilities, intersections (&) and spread. For both cases, you need to know how these work with exact types. Indeed spreading a non exact type into a type will make its keys nullable, while an intersection of exact types is always impossible!

Composition and exact types — Flow try

When using unions, quite often you’ll end up building your unions on the fly, some having maybe types. You’ll then check the existence of some property to determine which case you’re evaluating, but what you should aim for are tagged unions, using a unique property tagging the union. It’ll make your life easier. It’s often possible to do otherwise, but tagging your union is quite convenient (it’s for instance the way Redux works naturally).

Using tagged union to differentiate between the different members of a union — Flow try

I find it interesting to observe that this leads to code closer from what you’ll see in functional languages. In OCaml, Rust and co, you don’t create your unions/variants/enums (its mostly the same thing) on the fly, you explicitly create the variant. For instance, you don’t create a color and gives it props that make it the red color. The languages forces you to do so, and nothing prevent you from doing so in JS. It’ll be more explicit, and flow will automatically understand the types.

Use constructors for your complex types — Flow try

See how in Rust we have something similar (but built in the language):

Rust heavily uses variant, which are always explicitly created

Impossible states

Flow doesn’t care about the implicit ways of using your API. If there are some combination of props to your component that don’t make sense, flow won’t be happy. Let’s say that a given prop of your component is required when another one is set. Most of the time, this requirement is not written, it is implicit, as is “don’t worry, you don’t check if this prop is defined , since this other on is”.

Flow cannot handle implicit invariants, but is it a bad thing? — Flow try

I consider this a bad practice. Your codebase now only works if people are aware of this obscure knowledge. Good thing that flow doesn’t accept this kind of behaviour, right?

I can think of two ways to handle these cases. The first one, consists of making the impossible states impossible. See for instance this blog post by the great Kent C. Dodds. This can consist of redefining your API (split your component in two, where one requires both properties). Use enums. Finite state machines are also a way to go when the impossible cases revolve around having a combination of some state values.

Sometimes, it’s too complex to handle this using the component API. Then, using invariants and throwing errors is a better solution than falling back to wrong typings (as a reminder, an invariant is a condition that is always true any given moment of the execution of the program).

If you want to show flow that you know a given state is impossible, use invariants — Flow try

I really appreciate this pattern. It makes things explicit, and if you misuse your component, your error will be explicit! Moreover, it’s a great escape hatch for cases hard (or impossible) to type. When flow doesn’t agree with my typings, throwing an error will make him happy.

By the way, when you’re struggling with some typings, don’t be afraid of the error suppressing comment, $FlowFixMe (with a comment explaining the issue of course). I’ve stumbled many times on annotations lowering Flow’s understanding of your code (think of any, *, Object types, or weirder things with $Shape). Yes flow won’t complain, and your build will pass. But what is the point of that? This will kill other errors, that could be correct. And it won’t help the next person reading your code. On the other end, a $FlowFixMe (but please explain the reason why you put it), is both precise and explicit.

Don’t be afraid of FlowFixMe. And use it with type casting to show where the error really is — Flow try

What about React?

I cannot recommend enough the official doc, which goes in details about typing a component’s props, its state, or how to use defaultProps. But just as reminder, type props with a default as always there, Flow will take care of making them optional on the call-site.

defaultProps works great. Just use them as described in the doc — Flow try

Another cool thing with Props is the possibility to use exact types. While it’ll prevent you from passing unimportant props (which, depending on the way you write your component, can be pretty painful), but it’ll prevent the case where you’ll make a typo with an optional prop. Indeed, passing a colour prop instead of color is not an error if color is an optional prop. But it’s nice to be able to catch this.

With exact props, you won’t be able to make typos in you component props! — Flow try

Overall, there are not many differences between react and the rest of your application, react being JS without much magic (for your information, react typings are declared in the flow repository, and not in React’s one, which mean that to use the newest react features, you need to use the newest flow). However, some things are harder to type than others.

First of all, stay away from High Order Components (HOC) modifying props! I beg you! They are hard to type, especially when it comes to default props. But if you have some wrapped components, it is simpler to retype the component returned by the HOC. It’s cumbersome, but it is usually easy to do, and accurate.

Hocs are hard to type, it’s simpler to type the created component — Flow try

This last statement is actually valid for all components created with a function that flow cannot infer, components returned by factories for instance, which is pretty common when using CSS-in-JS.

If you use component factories, as with hoc, just retype the component — Flow try

On the other hand, render props are so much easier to type. Type the parameter of your render (or children) prop function, return a React.Node, and it’s done. Another benefit is that you don’t have to add the typing to the injected prop in your component props.

Render Props are easy and less verbose to type — Flow try

When using createContext, where your context is not optional and where you cannot use a default context, it’s better to wrap your consumer into a component asserting that the context pass is always defined, and use a null value as default, than passing a weirdly shaped default value which will make flow happy, without making any sense (these famous empty strings).

Don’t use phony default value for your context — Flow try

And as a bonus example, use generics, they are not that hard (they’re beyond the scope of this article, and people have already written a lot of things about them).

Don’t be afraid of generics — Flow try

When to use Flow

Apart from throw away projects, I believe that using a static type checker is necessary. I won’t discuss the difference with Typescript, I think that both are fine and work well enough. However, like any tool, it requires some investment, and for flow, it’s a continuous one.

In my opinion, embrace flow or drop it. I’ve seen teams passing more time circumventing the checker than thinking of their types. Too often, the development process becomes “I’ll write anything that silences flow”. Sadly, it just doesn’t make the error go away, it worsens the overall types of your codebase, which will force you to write more and more of these wrong types. And at the end, you spend your time muting flow errors, without getting any of its safety.

While the state I just described will more easily happen when gradually adopting flow, it is not necessary. But making it right requires discipline. And that’s hard to find in teams who don’t appreciate the tool.

Another thing that hinders teams is the overuse of helper functions and libraries like lodash, ramda or underscore for instance. While some of them publish typings, these aren’t perfect. And they’ll often silently lose the type of your code. On the other hand, since I believe that such libraries are most of the time non needed (if Sebastian Markbåge says it, then it’s true), it’s not a drawback.

Lastly, please don’t go for an overly defensive programming style because you’re not sure of your typings. Don’t have if checks everywhere because you’re not sure. Trust and enforce your types. Your type is written as nullable, but shouldn’t ? Don’t add an if check, remove the nullability. Sometimes it’ll force you to change something else, but I prefer to be too strict than not enough.

Closing thoughts

Flow is a bit like this developer in your team that keeps pointing out things that you know you should do but you don’t want to. Don’t fight it.

But if you don’t understand why it complains, you can still reach me on Twitter! You can also read this post on my personal blog where all my future posts will be.