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 https://flow.org/try, so you can play with the code to better understand why it works this way. And here is a repo with all the examples.
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.
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.
This also applies to arrays.
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.
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!
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).
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.
See how in Rust we have something similar (but built in the language):
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”.
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).
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.
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.
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.
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.
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.
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.
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).
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).
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.
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.