A Work in Progress

developer's blog by 
Credits

Making Flow Happy after 0.85

April 17, 2019

Revised on June 11, 2019

For the past few weeks I’ve filled my free hours to upgrade our codebase’s dependency of Flow to its latest versions. We were stuck at 0.83 because apparently anybody who tried 0.85 immediately decided to give up. When I picked it up, Flow was at 0.89. The time I’m writing this, it is at 0.97 already 🤷🏻‍♀️

What’s so hard about it

I’m assuming that since this title concerns you, you already know this fact:

After 0.85, all my connects are broken.

What gravitates the situation for our codebase is that we are a team of fans of higher order components. Our components are commonly wrapped with a series of nested higher order components that we even steal Redux’s compose for such wrapping. We thought we were smart? Wait until Flow starts to complain about it 😱.

On the side, the lack of guidance on this matter really baffles me the most. Is it too easy for everybody else or has everybody given up? At some point I decided that I’ll figure out the math by myself and I realized that there is no “quick setup” for me to play around.

Not to mention that our codebase is living thing,

living things got 💩 in it,

and there are some good hundreds of commits merged in to master every week so I’m also racing with time.

Yesterday, I finally wrapped up my batches of fixes to the first of our three main packages. Seeing the numbers shrink from 723 to 300-ish, then to two-digits, single digit, and the final “No errors!” console message was simply therapeutic.

So here are some lessons learned I wish I knew before I started.

Understand what is causing the error

Flow team published this article alongside the 0.85 update. Not all updates require a close examination on the release notes. But this is one you cannot miss.

Asking for required annotations

On the plus side, the post explained the situation really well and you shall read alongside if you are working on getting past 0.85. Since that post is fairly well-written, let me just quickly go over its key point.

Why was the change needed

I was not very proficient on the topics about Flow to begin with. I’ve read that article multiple times before and during the process. I hope I now understand the situation correctly: deleted because it is not helping anything

Flow delayed the type inference that allows it to catch type errors on input positions cross strand of file imports. But in order to do that, it requires that we annotate the input positions within each import - export cycle, or say the scope of each file. Then, it will be able to link each of those cycles and be aware of discrepancies that it previously was unable to.

The post Asking for Required Annotations has a simplified example. It may be helpful comprehending the situation here.

What explicitly are the “input positions” that require annotation

The post introduces the three types of “input positions” it refers to:

  • Function arguments
  • Writeable fields of classes
  • “Implicitly instantiated” function or class instances

The third one is probably causing the most number of unhappiness. And in particular, the most most most unhappy of all are perhaps connected components with React Redux. Unhappinesses of similar form extend to but do not limit to, createReducer, generic function calls, and most higher order components we wrote.

Where and how you can annotate them

Flow’s post suggests two places to annotate them. Its inconspicuous appearance does not match its paramount importance at all and I’ll have to put it here again:

We can fix this error by annotating the return type of the function call Try Flow or by providing an explicit type argument to the function call Try Flow.

Both are valid fixes. Both have legit use cases, which I’ll exemplify in the next section.

The actual process of making Flow happy

I suggest that you use your IDE’s Flow plugin for this process. With VSCode’s Flow extension set up correctly, it shows all the flow errors in one dialog grouped and ordered by files in alphabetical order. This makes it much easier to move around than 4000+ lines of command line outputs.

Search for keyword: implicitly instantiated

Once again if your IDE is properly set up this would make it much easier. If not, a piped grep should also work:

$ yarn flow | grep 'implicitly instantiated' -B 4 -A 10

The -B 4 modifier gets you 4 lines before the error report, which will likely tell you which file is unhappy. The -A 10 gets the 10 lines after, which should cover most of the error information.

Error -------------------------------------------------------------------------------- ../path/to/unhappiness/index.js

Missing type annotation for `SP`. `SP` is a type parameter declared in function type [1] and was implicitly instantiated
at call of `connect` [2].

    ../path/to/unhappiness/index.js:66:10
                v-------
    66| return connect(
    67|   mapState,
    68|   mapDispatch
    69| )(ComponentWithUnhappiness);
        ^ [2]

Here it is complaining that the exported connected component is not annotated. I think the psychological effect of having so many new vocabulary (implicitly, and instantiated) coming in in such big quantity (hundreds) is traumatic. But this is in effect the same type of error as this:

export function add(a, b) {
                    ^ Missing type annotation for `a`.
  return a + b;
}

Your list of errors may grow as you fix the implicit instantiation complaints

When I worked on our first main package, this confused me for the longest time, because, after I annotate the implicitly instantiated unhappiness, that error goes away. But, because of the added security of Flow in this version, it might have found some more unhappiness elsewhere, buried in hundreds of others. And if the error concerns an object of many fields that were improperly annotated, the number can jump in digits.

It’s scary, you know. After you fix one line, you’re not making Flow less unhappy, but you actually made it depressed 😭.

Anyway, here’s what I really want the me two months ago to know:

  • We’re on the right track, the new errors are due to exactly that we’re now properly annotating the input positions
  • Later on Flow will actually give us better error messages that makes it all worth it (or so I thought).

So hang in there..

Two places where you may want to annotate connect, but one can be better than the other

Following Flow’s blog post’s suggestion, and translated to this situation, here are the two ways to do it.

First, by providing an explicit type argument. And depending on which Flow-Typed annotation for React Redux you are using, your annotation may be different. Is there an official way of annotating it? Hmm, seems no. But it seems that the Flow-Typed’s test files are a good source of learning.

We are using the latest version react-redux_v5.x.x.js, which requires six type parameters and in which only two are essentially needed for the most common usages of connect.

export default connect<
  Props,
  OwnProps, // <- take out props fed in by connect
  _,
  _,
  _,
  _,
>(
  mapState,
  mapDispatch
)(MyHappyComponentNow);

This is a close-to-official declaration of how you may tell connect what the props of your connected component are. So you get the refined type security based on the inference there. It works. And I should not have much complaint about it except for an earlier mentioned fact that our components are commonly wrapped with multiple layers of hocs.

export default compose(
  withA,
  withB,
  withC,
  connect(
    mapState,
    mapDispatch
  )
)(FlowIsUnhappyAboutMyComponentAgain)

Now, whether that is a good practice or not is a out of the question. Even if it’s not evil I cannot rewrite the features for everyone.

And, on a side note, I tweeted a Try Flow about the fact that in order to properly annotate nested higher order components, each layer needs to take out the injected props that were taken care of by the previous layer 😩. This is, beyond practical.

import { compose } from "redux";

type AllTheProps = {/** ... */};

compose(
  withA<$Diff<$Diff<$Diff<AllTheProps, withDProps>, withCProps>, withBProps>>,
  withB<$Diff<$Diff<AllTheProps, withDProps>, withCProps>>,
  withC<$Diff<AllTheProps, withDProps>>,
  withD<AllTheProps>
)(ComponentWithEverything);

After a long trap into trying to annotate each layer of the hoc, it occured to me one day that I don’t have to annotate each of those layers at all. All I need to do is to declare once at file export, with the props needed when that exported component gets instantiated externally. This normally means the props minus all props injected by the higher order components.

import withA from 'path/to/withA'
import withB from 'path/to/withB'
import withC from 'path/to/withC'

import type { withAProps } from 'path/to/withA'
import type { withBProps } from 'path/to/withB'
import type { withCProps } from 'path/to/withC'

type Props = {
  /** component props */
}

type AllProps = Props & withAProps & withBProps & withCProps

const MyComponent = (props: AllProps) => {
  // ...
}

export default (compose(
  withA,
  withB,
  withC
)(FlowIsUnhappyAboutMyComponentAgain): React.AbstractComponent<Props>)

Changed I’ve updated the example above to match format of the previous one

So I’m left with the second method, by annotating the return type.

It does not concern how each layer of the HOCs decompose the props. It annotates only the final, exported component, which should have all the props anyway. So you can simply put the component’s Props that you should already have written anyway with earlier versions of Flow.

In fact, in my opinion this is exactly what is called for by 0.85. The function type parameters are consumed by higher order components to figure out what is the return type for them anyway. In this nested case there is no need to be explicit between those layers. The only thing Flow asks for is the annotated props of the final, composed component.

Benefit of doing all this

It can be too much of an ordeal that we forget about what was the original point. Now Flow gains the capability to infer along chains of file imports and it’s time that we look at the actual benefit.

example
example

Now, Flow actually outputs a side-by-side comparison of the two places where it finds a discrepancy on the types. 🎉

In fact, the list of errors that grew as we annotate the implicit instantiations, are then very clearly listed in this manner and are most likely easy to fix.

When to annotate and when to $FlowFixMe

Last week, the Flow team posted a new article that teaches me how to automatically add suppress messages.

Upgrading Flow Codebases

I’d do it if I knew it earlier. But since I’ve gone through most of the pain already, I’d say I now have some attitude about what to annotate and what to suppress.

  • Properly annotate higher order components if possible

    When some logic is abstracted out to a higher order component, it is meant to be shared. If possible, you should annotate those properly so that it has the capability of facilitating inference and not break the chain it may sit in.

  • Suppress the instances of the components wrapped by higher order components

    You know, they change more often. And they may be refactored. Tomorrow your team mate may be re-writing them with hooks. 🤷🏻‍♀️

Other issues

There are a few other issues that have blocked me here and there. They are not directly related with Flow but may slow down the process as well. I’ll list those issues here and I may follow up with some further notes if needed.

  • If you use prettier and/or eslint, the prettier and eslint-plugin-prettier need to accept Flow’s function type parameters
  • Making VSCode’s Flow plugin work

Wrapping Up

So I’ve picked up a task not knowing what I signed up for. There was a very steep learning curve and not much literature around. When I eventually realize what I did not understand, it becomes something I must write about.

To summarize,

  • You should upgrade Flow past 0.85, this is a major gain on type safety
  • Annotate at function return is often easier
  • Do not panic when the list of errors grow, the additional errors are the actual, meaningful errors and are less abstract and therefore easier to fix

References

Subscribe to my newsletter