Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

Flow typings #241

Closed
wants to merge 5 commits into from
Closed

Flow typings #241

wants to merge 5 commits into from

Conversation

gcanti
Copy link

@gcanti gcanti commented Sep 13, 2016

This PR is a proof of concept and is not meant to be merged.

There are not many examples of HOC typings with Flow out there, so I thought to write this POC.
I hope is helpful as a starting point for an official recompose libdef.

Feedback from recompose + flow users would be much appreciated.

@farism farism mentioned this pull request Sep 15, 2016
@acdlite
Copy link
Owner

acdlite commented Sep 18, 2016

!!!!

This is great! I will review this ASAP

@gcanti
Copy link
Author

gcanti commented Sep 18, 2016

@acdlite these are some "manual" tests I wrote while developing the libdef. They are not meant to be exhaustive (I'm sure I missed something), though they can be helpful in order to quickly try out the libdef

import React from 'react'
import ReactDOM from 'react-dom'
import type { Component } from 'recompose'
import {
  compose,
  mapProps,
  withProps,
  withHandlers,
  defaultProps,
  renameProp,
  renameProps,
  flattenProp,
  withState
} from 'recompose'

const app = document.getElementById('app')

type Props = {
  a: string,
  b: number
};

const C1: Component<Props> = (props: Props) => <div><pre>{JSON.stringify(props, null, 2)}</pre></div>

// mapProps
const C2 = mapProps(({ a }: $Shape<{ a: string }>): Props => ({ a, b: a.length }))(C1)

ReactDOM.render(<C2 a="s" />, app) // <= <C2 a="s" xxx="s" /> raises an error

// withProps
const C3 = withProps({ a: 's', b: 1 })(C1)

ReactDOM.render(<C3 />, app)

// withHandlers
const C5_1 = (props: { onChange: Function }) => <div><pre>{JSON.stringify(props, null, 2)}</pre></div>
const C5 = withHandlers({
  onChange: props => event => {
    props.updateValue(event.target.value)
  }
})(C5_1)

ReactDOM.render(<C5 />, app)

// defaultProps
const C6 = defaultProps({ a: 's' })(C1)

ReactDOM.render(<C6 b={1} />, app)

// renameProp
const C7: Component<{cc: number}> = renameProp('a', 'cc')(C1)

ReactDOM.render(<C7 cc={1} />, app)

// renameProps
const C8: Component<{ccc: number}> = renameProps({'a': 'ccc'})(C1)

ReactDOM.render(<C8 ccc={1} />, app)

// flattenProp
const C9: Component<{}> = compose(
  withProps({
    object: { a: 'a', b: 'b' },
    c: 'c'
  }),
  flattenProp('object') // <= flattenProp('xxx') raises an error
)(C1)

ReactDOM.render(<C9 />, app)

// withState
const C10_1 = (props: { counter: number }) => <div><pre>{JSON.stringify(props, null, 2)}</pre></div>
const C10 = withState(
  'counter',
  'setCounter',
  0
)(C10_1)

ReactDOM.render(<C10 c="c" />, app)

@hannupekka hannupekka mentioned this pull request Oct 4, 2016
@hannupekka
Copy link

hannupekka commented Oct 4, 2016

When using pure with this rather simple component:

// @flow
import React from 'react';
import { pure } from 'recompose';

const logo = require('assets/logo.svg');

const Logo = (props: { className?: string }) => {
  return (
    <div className={props.className}>
      <img src={logo} />
    </div>
  );
};

export default pure(Logo);

Results:

src/components/Login.js:68
 68:         <Logo />
             ^^^^^^^^^^^^^^^^^^^^^^^^^ class type: type application of identifier `React$Component`. This type is incompatible with
 68:         <Logo />
             ^^^^^^^^^^^^^^^^^^^^^^^^^ React$Element

src/components/Login.js:68
 68:         <Logo />
             ^^^^^^^^^^^^^^^^^^^^^^^^^ function type. This type is incompatible with
 68:         <Logo />
             ^^^^^^^^^^^^^^^^^^^^^^^^^ React$Element

src/components/Login.js:68
 68:         <Logo />
             ^^^^^^^^^^^^^^^^^^^^^^^^^ React element `Logo`
 68:         <Logo />
             ^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Logo`. This type is incompatible with
 11:   declare type HOC<A, B> = Fn1<Component<A>, Component<B>>;
                                    ^^^^^^^^^^^^ union: type application of polymorphic type: type `FunctionComponent` | type application of polymorphic type: type `ClassComponent`. See lib: tools/flow/types/Recompose.js:11
  Member 1:
    7:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                     ^^^^^^^^^^^^^^^^^^^^ type application of polymorphic type: type `FunctionComponent`. See lib: tools/flow/types/Recompose.js:7
  Error:
    7:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                     ^^^^^^^^^^^^^^^^^^^^ function type. Callable signature not found in. See lib: tools/flow/types/Recompose.js:7
   68:         <Logo />
               ^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Logo`
  Member 2:
    7:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^ type application of polymorphic type: type `ClassComponent`. See lib: tools/flow/types/Recompose.js:7
  Error:
   68:         <Logo />
               ^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Logo`. This type is incompatible with
    7:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^ class type: type application of identifier `React$Component`. See lib: tools/flow/types/Recompose.js:7


Found 3 errors

Thank you for your work with this PR!

@gcanti
Copy link
Author

gcanti commented Oct 4, 2016

Oh sorry, I think there is an error in my libdef, could you please try this change?

-declare function pure<A>(): HOC<A, A>;
+declare function pure<A>(C: Component<A>): FunctionComponent<A>;

@gcanti
Copy link
Author

gcanti commented Oct 4, 2016

@hannupekka just to be sure, you are calling Logo like this, right?

import React from 'react';
import Logo from './Logo';

<Logo styleName="logo" />

@hannupekka
Copy link

hannupekka commented Oct 4, 2016

@gcanti sweet, works as expected!

And yes, calling it just like you described.

@gcanti
Copy link
Author

gcanti commented Oct 4, 2016

@hannupekka some observations

If you call

<Logo className={1} /> // <= wrong type for className

Flow doesn't complain, right? (But it should)

The problem is that, due to the current behavior of Flow, you must explicitly type the exported APIs. So my suggestion is to change the definition of Logo to this

// @flow

import React from 'react';
import { pure } from 'recompose';
import type { Component } from 'recompose';

const logo = require('assets/logo.svg');

type Props = {
  className?: string
};

const Logo: Component<Props> = (props) => {
  return (
    <div className={props.className}>
      <img src={logo} />
    </div>
  );
};

export default pure(Logo);

It's a little more verbose but it makes sure you get the best type safety.

<Logo className={1} /> // <= error: number. This type is incompatible with string

Now if you want to go further and you are using Flow v0.32+ you can even prevent additional props, just change the Props definition to

type Props = $Exact<{
  className?: string
}>;

and you get

<Logo unknown="prop" /> // <= error: property `unknown`. Property not found in object type 

With the latest babel / babel-eslint you can even use a new syntax (equivalent to the $Exact magic type)

type Props = {|
  className?: string
|};

@hannupekka
Copy link

hannupekka commented Oct 4, 2016

True, nice catch!

After that ESLint might give warnings depending on the config and plugins you are using:

error 'className' is missing in props validation react/prop-types

To fix:

  • const Logo: Component<Props> = (props) => --> const Logo: Component<Props> = (props: Props) =>
  • Disable ESLint rule no-duplicate-imports
  • If not already using eslint-plugin-import, install it and enable no-duplicates (if necessary)

@wuct
Copy link
Contributor

wuct commented Oct 31, 2016

Thank you @gcanti! I've been trying the declaration file. When using withProps with a function argument:

type Props = {
  a: string,
  b: number,
};


const C1: Component<Props> = (props) => <div />

const C3 = withProps(() => ({ a: '1', b: 2 }))(C1)

It throws an error:

 21: const C3 = withProps(<Props>() => ({ a: '1', b: 2 }))(C1)
                                                           ^^ function type. This type is incompatible with
 14:   declare type HOC<A, B> = Fn1<Component<A>, Component<B>>;
                                    ^^^^^^^^^^^^ union: type application of polymorphic type: type `FunctionComponent` | type application of polymorphic type: type `ClassComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:14
  Member 1:
   10:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                     ^^^^^^^^^^^^^^^^^^^^ type application of polymorphic type: type `FunctionComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:10
  Error:
   19: const C1: Component<Props> = (props) => <div />
                           ^^^^^ property `a` of object type. Property not found in
   21: const C3 = withProps(<Props>() => ({ a: '1', b: 2 }))(C1)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ arrow function
  Member 2:
   10:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^ type application of polymorphic type: type `ClassComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:10
  Error:
  inconsistent use of library definitions
    6:   declare type FunctionComponent<A> = (props: A) => ?React$Element<any>;
                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics of function type. Callable signature not found in. See lib: flow-typed/npm/recompose_vx.x.x.js:6
   16: declare class React$Component<DefaultProps, Props, State> {
       ^ statics of React$Component. See lib: /private/tmp/flow/flowlib_c064d16/react.js:16

I am not familiar with flow, so I am not sure how to fix it. I've tried to annotate the function argument but without luck:

const C3 = withProps((): Props => ({ a: '1', b: 2 }))(C1) // still error

@gcanti
Copy link
Author

gcanti commented Oct 31, 2016

@wuct Flow seems to not account for the second member of the union

declare function withProps<A, B>(
  createProps: A | (ownerProps: B) => A
): HOC<A, B>;

try this instead (explicit overloading)

declare function withProps<A, B>(
  createProps: (ownerProps: B) => A
): HOC<A, B>;
declare function withProps<A>(
  createProps: A
): HOC<A, {}>;

@wuct
Copy link
Contributor

wuct commented Nov 1, 2016

@gcanti Thanks! But now it works like mapProps. withProps should be able to return partial props of the base component.

If I remove a prop returned by withProps:

type Props = {
  a: string,
  b: number,
};

const enhance =
  withProps(() => ({ a: '1' })) // remove b

const C1: Component<Props> = (props) => <div />

const C3 = enhance(C1)

Flow throws an error:

 17: const C3 = enhance(C1)
                        ^^ function type. This type is incompatible with
 14:   declare type HOC<A, B> = Fn1<Component<A>, Component<B>>;
                                    ^^^^^^^^^^^^ union: type application of polymorphic type: type `FunctionComponent` | type application of polymorphic type: type `ClassComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:14
  Member 1:
   10:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                     ^^^^^^^^^^^^^^^^^^^^ type application of polymorphic type: type `FunctionComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:10
  Error:
   15: const C1: Component<Props> = (props) => <div />
                           ^^^^^ property `b`. Property not found in
   13:   withProps(() => ({ a: '1' }))
                          ^^^^^^^^^^ object literal
  Member 2:
   10:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^ type application of polymorphic type: type `ClassComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:10
  Error:
  inconsistent use of library definitions
    6:   declare type FunctionComponent<A> = (props: A) => ?React$Element<any>;
                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics of function type. Callable signature not found in. See lib: flow-typed/npm/recompose_vx.x.x.js:6
   16: declare class React$Component<DefaultProps, Props, State> {
       ^ statics of React$Component. See lib: /private/tmp/flow/flowlib_14902df2/react.js:16

I think we can use $Shape to tell flow just checks partial types, so I modify the declaration:

declare function withProps<A, B>(
  createProps: (ownerProps: B) => $Shape<A>
): HOC<A, B>;

However, flow become undecided:

 17: const C3 = enhance(C1)
                        ^^ function type. This type is incompatible with
 14:   declare type HOC<A, B> = Fn1<Component<A>, Component<B>>;
                                    ^^^^^^^^^^^^ union: type application of polymorphic type: type `FunctionComponent` | type application of polymorphic type: type `ClassComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:14
  Member 1:
   10:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                     ^^^^^^^^^^^^^^^^^^^^ type application of polymorphic type: type `FunctionComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:10
  Error:
   15: const C1: Component<Props> = (props) => <div />
                           ^^^^^ property `b`. Property not found in
   13:   withProps(() => ({ a: '1' }))
                          ^^^^^^^^^^ object literal
  Member 2:
   10:   declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^ type application of polymorphic type: type `ClassComponent`. See lib: flow-typed/npm/recompose_vx.x.x.js:10
  Error:
  inconsistent use of library definitions
    6:   declare type FunctionComponent<A> = (props: A) => ?React$Element<any>;
                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics of function type. Callable signature not found in. See lib: flow-typed/npm/recompose_vx.x.x.js:6
   16: declare class React$Component<DefaultProps, Props, State> {
       ^ statics of React$Component. See lib: /private/tmp/flow/flowlib_14902df2/react.js:16

Am I using $Shape correctly?

@gcanti
Copy link
Author

gcanti commented Nov 1, 2016

withProps should be able to return partial props of the base component

Ah, you are right. This seems to work as expected

declare function withProps<A, B>(
  createProps: (ownerProps: B) => A
): HOC<A & B, B>;
declare function withProps<A, B>(
  createProps: A
): HOC<A & B, B>;

Here my tests

const C3_1 = withProps({ a: 's', b: 1 })(C1)
// ok
;<C3_1 />

const C3_2 = withProps({ b: 1 })(C1)
// ok
;<C3_2 a="s" />
// ok
;<C3_2 a="s" b={2} />
// $ExpectError missing property a
;<C3_2 />

const C3_3 = withProps(() => ({ a: 's', b: 1 }))(C1)
// ok
;<C3_3 />

const C3_4 = withProps(() => ({ b: 1 }))(C1)
// ok
;<C3_4 a="s" />
// ok
;<C3_4 a="s" b={2} />
// $ExpectError missing property a
;<C3_4 />

@wuct
Copy link
Contributor

wuct commented Nov 1, 2016

@gcanti It works! I haven't come up with the intersection type. Thanks a lot!

@gcanti
Copy link
Author

gcanti commented Nov 1, 2016

@wuct thanks for testing out. PR updated

@wuct
Copy link
Contributor

wuct commented Nov 18, 2016

It turns out that we shall also use the intersection type in withHandler

  declare function withHandlers<B, A: { [key: string]: (props: B) => Function }>(
    handlerCreators: A
  ): HOC<A & B, B>

@gcanti
Copy link
Author

gcanti commented Nov 18, 2016

Thanks @wuct. PR updated.

@istarkov
Copy link
Contributor

istarkov commented Nov 24, 2016

It's awesome!!!

Wanna ask how we could change the enhancers like withState as current is not helpful with flowtype.

Just played a little with dirty idea like

  declare function withState2<B, T, A: { [key: string]: (props: B) => T }>(
    states: A
  ): HOC<{ [key: $Keys<A>]: { value: T, setValue: (a: T) => void } } & B, B>

usage example

const C1 = ({ foo: { value, setValue } }) => (
  <div onClick={() => setValue('world')}>
    {value}
  </div>
);

const C2  = withState2({
  foo: ({ fooInitial }) => fooInitial,
})(C1)

<C fooInitial="hello" />

It works nice with flow having that all state initializers inside withState2 must have similar types,
(with different types it resolves them to |)

@ctrlplusb
Copy link

@istarkov do you reckon this definition as it stands is fairly useful/friendly to the current recompose implementation? i.e. could I manually add them to my project and get by okay, or do you reckon they need more work still?

@istarkov
Copy link
Contributor

I've played with this locally and I really like this definitions, BTW I does not use flowtype in my everyday work so can't be an expert. IMO this must be publushed somehow, may be in flowtyped repo, or here, but I really have zero knowledge how to do it, and does just merging this and publishing is enough.

Also some documentation and tests are also needed here. If anyone can help with please welcome.

@ctrlplusb
Copy link

Personally I feel that the official flow-typed repo is the way to go. I have experience publishing there. They allow you to iterate on definitions so they need not be 100% to begin with (although they should be respectable and backed with tests).

@gcanti - are you happy if we move this on to the flow-typed repo?

I'm happy to help out here as I am adopting recompose for a large scale project.

@gcanti
Copy link
Author

gcanti commented Dec 13, 2016

are you happy if we move this on to the flow-typed repo?

Sure 👍

@namuol
Copy link

namuol commented Dec 21, 2016

@ctrlplusb @gcanti:

Has the discussion continued on the flow-typed repo? I don't see anything mentioning recompose there.

@ctrlplusb
Copy link

@namuol not as of yet.

@gcanti - would you like to open a PR there, or are you looking for some help with this? :)

@gcanti
Copy link
Author

gcanti commented Dec 22, 2016

I have experience publishing there
I am adopting recompose for a large scale project

@ctrlplusb please, feel free to take this PR as a starting point and go ahead, you are in a better position than me ;)

@wuct
Copy link
Contributor

wuct commented Dec 29, 2016

After [email protected], withHandlers accepts a factory function, so its type need to be updated as:

  declare function withHandlers<B, A: { [key: string]: (props: B) => Function }>(
    handlerCreators: (ownerProps: B) => A
  ): HOC<A & B, B>

  declare function withHandlers<B, A: { [key: string]: (props: B) => Function }>(
    handlerCreators: A
  ): HOC<A & B, B>

@istarkov
Copy link
Contributor

istarkov commented Dec 30, 2016

I've played with current definitions for typescript for recompose, and wanna say that current is much more nicer.

Returning to withState, what are you think about creating a similar to withState enhancer, but with signature.

withValue<A, B, T>(
  initialState: T | (props: Object) => T , 
  mapStateProps: (state: T, setState: (T) => void | etc.. ) => A
): HOC<A & B, B>;

Haven't played with but IMO it will allow to have typed definition of withState

Usage example:

// old
withState('value', 'onChange', 1)
// new
withValue(1, (value, onChange) => ({ value, onChange }));

@wuct
Copy link
Contributor

wuct commented Dec 30, 2016

@istarkov by current, do you mean Flowtype definition? I haven't played with Typescript so I don't know what is the difference between them :)

I like the new version of withValue, the signature is much cleaner than the previous one. Great work! I'm thinking that we can just replace the old withState with the new one, because they do the same thing. Providing two similar HoC will be confusing.

@istarkov
Copy link
Contributor

Yes flowtype definition.
About name IMO it will be good to name it the same, BTW still allow prev version to work, with deprecation warning, as I have enormous number of withState enhancers internally, and changing all of them in one step will be error prone I think.
Having deprecation warning, I could switch them one by one.
Also may be other fun ideas will be available, for now one thing I dislike - name duplication in
(blabla, setBlabla) => ({ blabla, setBlabla }) looks somehow strange ;-)

@joncursi
Copy link

joncursi commented Feb 23, 2017

I'm following @gcanti 's example above and I'm unable to get it to work for the compose function:

// button.js
const Button: Component<Props> = (props) => (
  <Button
    ...
  />
);

export default compose(
  onlyUpdateForKeys(['following']),
)(Button);



// view.js
<Button badProp={badValue} /> // Flow doesn't catch it

Interestingly enough, if compose is dropped off...

// button.js
const Button: Component<Props> = (props) => (
  <Button
    ...
  />
);

export default onlyUpdateForKeys(['following'])(Button);



// view.js
<Button badProp={badValue} /> // Flow catches it

... then it gets properly flagged.

Am I doing something wrong, or does this libdef not support chaining HOCs together via compose?

@joncursi
Copy link

Is it possible to manually specify the flow type of the enhanced component? I'm not sure what the type signature would look like for this:

// button.js
const Button: Component<Props> = (props) => (
  <Button
    ...
  />
);

const enhance: /* ??? */ = compose(
  onlyUpdateForKeys(['following']),
)(Button);

export default enhance;

I have tried:

const enhance: HOC<Button, Button> = compose(
  onlyUpdateForKeys(['following']),
);

export default enhance(Button);

but no luck there.

@idris
Copy link
Contributor

idris commented Jun 15, 2017

What's the status of this (or flow-typed/flow-typed#624)? Is this going to happen any time soon?

@istarkov
Copy link
Contributor

Seems like having modern flowtype some HOCs are better to write using Object spread
like withProps

 declare function withProps<BaseAdd, Enhanced>(
    propsMapper: (ownerProps: Enhanced) => BaseAdd
  ): HOC<{ ...$Exact<Enhanced>, ...BaseAdd }, Enhanced>;

this allows to change property types etc

Also seems like I have a few free days, and want to spend them to add typings to recompose.

Can someone recommend how to better expose flowtype declarations, use flowtyped repo, or add .flow.js to current repo?

@istarkov
Copy link
Contributor

As I see modern libs use flowtyped https://www.styled-components.com/docs/api#flow

@TrySound
Copy link
Contributor

@istarkov flow-typed sucks in siemens. It uses git requests. For npm we have artifactory.

@idris
Copy link
Contributor

idris commented Jun 29, 2017

@istarkov flow-typed is for libraries that don't support flow themselves. If you can use flow in the actual repo, that's better (through flow directly in the JS files OR .js.flow)

@istarkov
Copy link
Contributor

all, please take a look #428
@gcanti Thank you a lot for your contribution,
new PR is heavily based on it.

@istarkov istarkov closed this Jul 12, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants