Typing Higher-order Components in Recompose With Flow

One month ago Recompose landed an official Flow library definition. The definitions were a long time coming, considering the original PR was created by @GiulioCanti a year ago.

It took a while to land the definitions because of one problem. I got stuck typing one of simplest enhancers in the Recompose library: withProps.

Type inference worked, but not in a way I expected. Errors were not detected, error messages were cryptic, and the readability of intersection types didn’t make me happy. The only fix was to declare almost every input-output type for each enhancer but I’m too lazy to do that well. Playing with type definitions didn’t give me a better result, so I gave up.

The game changer was object type spread, which landed in Flow v0.42.0. Object type spread provides functionality for “spreading” two object types together with the same semantics as JavaScript’s runtime value spread. The syntax is also almost exactly the same:

type A = { ...B };
const a = { ...b };

Currently the object type spread logic only exists for the type spread syntax, but Flow will be using the same logic to implement spread for values in an upcoming version.

With object spread for types, a small output type definition change to the withProps enhancer gave me the expected result:

// HOC here is a higher-order component
// a function that takes a component and returns a new component.
type HOC<A, B> = (a: React.ComponentType<A>) =>
React.ComponentType<B>
// change output type of withProps
// from `HOC<A & B, B>` to `HOC<{ ...$Exact<B>, ...A }, B>`
// HOC here is a higher-order component
// a function that takes a component and returns a new component
// or in terms of flow: type HOC<A, B> =
// (a: React.ComponentType<A>) => React.ComponentType<B>
type EnhancedCompProps = { b: number };
const enhancer2: HOC<*, EnhancedCompProps> = compose(
withProps(({ b }) => ({
b: `${b}`,
})),
withProps(({ b }) => ({
// $ExpectError: The operand of an arithmetic
// operation must be a number.
c: 1 * b,
}))
)

I was able to declare just the enhanced component props type definition; then all types were inferred properly, and the error was readable.

It was clear that if it were possible to make the same idea work for most Recompose enhancers, it would be easy to work with Flow and Recompose.

And thanks to Flow type inference and @GiulioCanti, for most enhancers it already worked.

Meet Recompose + Flow

In most cases all you need to do is to declare a props type of enhanced component. Flow will infer all the other types you need.

// @flow
import * as React from 'react';
import {
compose,
defaultProps,
withProps,
type HOC,
} from 'recompose';
type EnhancedComponentProps = {
text?: string,
};
const baseComponent = ({ text }) => <div>{text}</div>;
const enhance: HOC<*, EnhancedComponentProps> = compose(
defaultProps({
text: 'world',
}),
withProps(({ text }) => ({
text: `Hello ${text}`
}))
);
const EnhancedComponent = enhance(baseComponent);
export default EnhancedComponent;

You don’t need to provide types for arguments to enhancers. Flow will infer them automatically.

Remove defaultProps in the example above and you would immediately get a Flow error:

The other wonderful feature is Flow’s ability to infer types automatically, which makes the Recompose experience even better. See it in action:

For me this feature is much cooler than error detection, as it allows you to read and understand the code much faster.

The magic above works for most Recompose enhancers, but not all. Any type system has its limitations, so for some enhancers you will need to provide type information for every special case (no automatic type inference), or use the following recommendations:

  • flattenProp, renameProp, renameProps—use withProps
  • withReducer, withState—use withStateHandlers
  • lifecycle—write your own enhancer instead; see this test for an example
  • mapPropsStream—see the test for an example

For an example of how to type enhancers like this, see this test.

Using Recompose and Flow with React class components

Sometimes you need to use Recompose with React class components. You can use the following helper to extract the property type from an enhancer:

// Extract type from any enhancer
type HOCBase_<A, B, C: HOC<A, B>> = A;
type HOCBase<C> = HOCBase_<*, *, C>;

And use it within your component declaration:

type MyComponentProps = HOCBase<typeof myEnhancer>;
class MyComponent extends React.Component<MyComponentProps> {
render() { /* ... */ }
}
const MyEnhancedComponent = myEnhancer(MyComponent);

Write your own enhancers

To write your own simple enhancer refer to the Flow documentation and you will end up with something like:

// @flow
import * as React from 'react';
import { compose, withProps, type HOC } from 'recompose';
function mapProps<BaseProps: {}, EnhancedProps>(
mapperFn: EnhancedProps => BaseProps,
): (React.ComponentType<BaseProps>) => React.ComponentType<EnhancedProps> {
return Component => props => <Component {...mapperFn(props)} />;
}
type EnhancedProps = { hello: string };
const enhancer: HOC<*, EnhancedProps> = compose(
mapProps(({ hello }) => ({
hello: `${hello} world`,
len: hello.length,
})),
withProps(props => ({
helloAndLen: `${props.hello} ${props.len}`,
})),
);

For class-based enhancers:

// @flow
import * as React from 'react';
import { compose, withProps, type HOC } from 'recompose';
function fetcher<Response: {}, Base: {}>(
dest: string,
nullRespType: ?Response,
): HOC<{ ...$Exact<Base>, data?: Response }, Base> {
return BaseComponent =>
class Fetcher
extends React.Component<Base, { data?: Response }> {
      state = { data: undefined };
      componentDidMount() {
fetch(dest)
.then(r => r.json())
.then((data: Response) => this.setState({ data }));
}
      render() {
return (
<BaseComponent
{...this.props}
{...this.state}
/>
);
}
}
type EnhancedCompProps = { b: number };
type FetchResponseType = { hello: string, world: number };
const enhancer: HOC<*, EnhancedCompProps> = compose(
// pass response type via typed null
fetcher('http://endpoint.ep', (null: ?FetchResponseType)),
  // see here fully typed data
withProps(({ data }) => ({
data,
}))
);

Now Flow will infer the type of data properly, so you can safely use it.

Links

P.S.

Your move, Caleb ;-)

Editor’s Note: Done 😉 https://github.com/facebook/flow/commit/ab9bf44c725efd2ed6d7e1e957c5566b6eb6f688

Are you creating something cool with Recompose and Flow? We want to hear about it. Send a message to flowtype on Twitter so we can share what you are building with our community so that we all may celebrate your efforts to make writing JavaScript more delightful!
Like what you read? Give Ivan Starkov a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.