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

, , —use withReducer , withState —use withStateHandlers

, —use lifecycle —write your own enhancer instead; see this test for an example

—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

A fully-typed example project of this menu app

Usage examples can be found in tests

P.S.

Your move, Caleb ;-)