1
votes

I'm a newbie using flow so I might be doing some very stupid mistakes.

I want to create a Props flow type that intersects the props passed to a component, the props obtained from a redux store returned from mapStateToProps and some action creators defined on mapDispatchToProps.

All the code shown below is available on this flow try.

Let's consider that I have the following State and action creators:

State and Action Creators

// State type
type State = {
    state_text: string,
    state_num: number,
}

// Action type
type Action = 
    | { type: 'ACTION_CREATOR_1', text: string}
    | { type: 'ACTION_CREATOR_2', value: number}


// action creators
const actionCreator_1 = (): Action => ({
    type: 'ACTION_CREATOR_1',
    text: 'some text',
})
const actionCreator_2 = (num: number): Action => ({
    type: 'ACTION_CREATOR_2',
    value: num + 1,
})

I followed an example on this post, and I came to something like the following to define my types:

Prop type definition

const mapStateToProps = (state: State) => {
    const { state_text, state_num } = state
    return { state_text, state_num }
}

const mapDispatchToProps = {
    actionCreator_1,
    actionCreator_2,
}


// type extract return helper
type _ExtractReturn<B, F: (...args: any[]) => B> = B
type ExtractReturn<F> = _ExtractReturn<*, F>
  

// set combined types 
type ReduxState = ExtractReturn<typeof mapStateToProps>

type ReduxActions = typeof mapDispatchToProps

type OwnProps = { 
    own_string: string,
    own_num: number 
}

type Props =  OwnProps & ReduxState & ReduxActions

With this definition, I was expecting Props to be something like this:

type DesiredProp =   {|
    own_string: string,
    own_num: number,
    state_text: string,
    state_num: number,
    actionCreator_1: () => Action,
    actionCreator_2: (num: number) => Action,
|} 

However, the following situation shows no flow errors:

const props2: Props = {
    own_string: 'text',                 // OK - gives error if 123  
    own_num: 123,                       // OK - gives error if 'text'
    state_text: 123,                    // NOK - should be an error     
    state_num: 'text',                  // NOK - should be an error 
    actionCreator_1: actionCreator_1,   // OK - gives error if actionCreator_2
    actionCreator_2: actionCreator_1,   // NOK - should be an error 
    fakeActionCreator: () => {}         // NOK - should be an error 
}

I put all this code in this flow try to make it easier to understand and play with.

How can I achieve what I'm looking for?

1

1 Answers

5
votes

Well, it looks like you have a few things topics in your post, so I'll break up my answer

Returning a Disjoint Union Member

Here you're returning a member of the Action disjoint union

// Action type
type Action = 
  | { type: 'ACTION_CREATOR_1', text: string}
  | { type: 'ACTION_CREATOR_2', value: number}

// action creators
const actionCreator_1 = (): Action => ({
  type: 'ACTION_CREATOR_1',
  text: 'some text',
})

const actionCreator_2 = (num: number): Action => ({
  type: 'ACTION_CREATOR_2',
  value: num + 1,
})

And you expect it to be able to typecheck these lines:

actionCreator_1: actionCreator_1,   // OK - gives error if actionCreator_2
actionCreator_2: actionCreator_1,   // NOK - should be an error 

The thing is, you set the return type for the action creators to both be Action, so as long as one of your actionCreator_1/2 properties returns an Action, the code is correct. What you need to do here is to make the return type of the action creators more specific:

type Action1 = { type: 'ACTION_CREATOR_1', text: string}
type Action2 = { type: 'ACTION_CREATOR_2', value: number}

// Action type
type Action = 
  | Action1
  | Action2

// action creators
const actionCreator_1 = (): Action1 => ({
  type: 'ACTION_CREATOR_1',
  text: 'some text',
})

const actionCreator_2 = (num: number): Action2 => ({
  type: 'ACTION_CREATOR_2',
  value: num + 1,
})

And now flow will throw an error (try):

    76:     actionCreator_2: actionCreator_1,   // NOK - should be an error 
                            ^ Cannot assign object literal to `props2` because property `value` is missing in `Action1` [1] but exists in `Action2` [2] in the return value of property `actionCreator_2`.
    References:
    18: const actionCreator_1 = (): Action1 => ({
                                    ^ [1]
    23: const actionCreator_2 = (num: number): Action2 => ({
                                               ^ [2]
    76:     actionCreator_2: actionCreator_1,   // NOK - should be an error 
                            ^ Cannot assign object literal to `props2` because string literal `ACTION_CREATOR_1` [1] is incompatible with string literal `ACTION_CREATOR_2` [2] in property `type` of the return value of property `actionCreator_2`.
    References:
    9: type Action1 = { type: 'ACTION_CREATOR_1', text: string}
                              ^ [1]
    10: type Action2 = { type: 'ACTION_CREATOR_2', value: number}
                               ^ [2]

Exactly Specifying an Object

Right now you're taking an intersection of three objects to get your Props type. Since you're working with an inexact object (the result of the intersection), you don't get an exact result. First you need to adjust your OwnProps to be exact. Then you need to combine all the objects. I do this with a spread:

// set combined types 
type ReduxState = ExtractReturn<typeof mapStateToProps>
type ReduxActions = typeof mapDispatchToProps

// This object type needs to be exact
type OwnProps = {|
  own_string: string,
  own_num: number 
|}

// Use a spread to combine the exact objects into one
type Props =  {|
  ...OwnProps,
  ...ReduxState,
  ...ReduxActions,
|}

And now it throws an error for that pesky extra property (try):

    74: const props2: Props = {                          ^ Cannot assign object literal to `props2` because property `fakeActionCreator` is missing in `Props` [1] but exists in object literal [2].
    References:
    74: const props2: Props = {
                      ^ [1]
    74: const props2: Props = {                          ^ [2]

Extracted Type Interfering with Inference

Unfortunately, I can't seem to get flow to understand the return type of your ReduxState through return type extraction. It seems to think it's this:

type ReduxState = ExtractReturn<(state: State) => {|state_num: (string | number), state_text: (string | number)|}>

So it seems to be merging the inference from your usage in the props2 object and the extracted type information. To get around this, my best advice would be to manually type the mapStateToProps function's output:

const mapStateToProps = (state: State): {|
    state_text: string,
    state_num: number,
|} => {
  const { state_text, state_num } = state
  return { state_text, state_num }
}

After which it should throw an error (Try)

    80:     state_text: 123,                    // NOK - should be an error     
                     ^ Cannot assign object literal to `props2` because number [1] is incompatible with string [2] in property `state_text`.
    References:
    80:     state_text: 123,                    // NOK - should be an error     
                     ^ [1]
    34:     state_text: string,
                     ^ [2]
    81:     state_num: 'text',                  // NOK - should be an error 
                      ^ Cannot assign object literal to `props2` because string [1] is incompatible with number [2] in property `state_num`.
    References:
    81:     state_num: 'text',                  // NOK - should be an error 
                      ^ [1]
    35:     state_num: number,
                       ^ [2]