Flow: [React] Flow loses the inferred type of imported functions when using typeof inside Component<Props>

Created on 24 Aug 2018  Â·  11Comments  Â·  Source: facebook/flow

Edit: last update on this: it appears only when 'action' is imported.

--

I don't know if I'm doing something wrong or if it's a bug, but it looks pretty straightforward:

import { action } from 'somewhere'

// action: (param1: string) => {| type: string, param1: string |}

const actions = {
  action
}

type Props = typeof actions

class MyComponent extends Component<Props> {
  componentDidMount() {
    this.props.action(1, 2, 3) // No errors when I run 'flow check'!
  }
}

Flow correctly infers the type of the function outside of the component. But, once it is passed to the component (this.props.action), it becomes any.

I also tried, without success:

type Actions = typeof actions

type Props = Actions
type Props = {} & Actions
type Props = { ...Actions }
type Props = {| ...Actions |}

Most helpful comment

So I just discovered that it's actually because action is imported.

I was trying to reproduce the bug on flow.org/try but it works as expected when it is in the same file: https://flow.org/try/#0JYWwDg9gTgLgBAJQKYEMDGMA0cDecDCE4EAdkifAL5wBmURcA5FKhowFDtqkDO86MYKTgBeOAAowKKChABGAFxw+UYCQDmASlEA+CXhgBPMEiWMAgvgAqASQDyAOUbYpM+XEqbOAoSXFzsACZsAGYvdiMTOAAFejAeUVwAHzgAOnTzDF8EpMpONAAbFB4EgDEICDgkAA8YcgATBMJiMgoAHliIeL0cdjg4bhbyGAARYHqAWQgAVwpxbV7+-pgAC2AeVLA4jZ9SfyDQr368vK5efizeRMW4XZIlh8en55el9lPIpDhMwSuxT4gNFulxIPCAA

I also confirm that the type of the imported action is correctly inferred out of the component's props:

import { action } from '../state/actions'
// action: (param1: string) => {| type: string, param1: string |}

const mapActionsToProps = {
  action
}

type Actions = typeof mapActionsToProps

type Props = {| ...Actions |}

class MyComponent extends Component<Props> {
  componentDidMount() {
    action(1, 2, 3) // Error 1: Cannot call `action` with `1` bound to `param1` because number [1] is incompatible with string [2]
                    // Error 2: Cannot call `action` because no more than 1 argument is expected by function [1]

    this.props.action(1, 2, 3) // No errors!
  }
}

And if in the same file, it works as expected:

const action = (param1: string) => ({ type: 'ACTION', param1 })
// action: (param1: string) => {| type: string, param1: string |}

const mapActionsToProps = {
  action
}

type Actions = typeof mapActionsToProps

type Props = {| ...Actions |}

class MyComponent extends Component<Props> {
  componentDidMount() {
    action(1, 2, 3) // Error 1: Cannot call `action` with `1` bound to `param1` because number [1] is incompatible with string [2]
                    // Error 2: Cannot call `action` because no more than 1 argument is expected by function [1]

    this.props.action(1, 2, 3) // Error 1: Cannot call `action` with `1` bound to `param1` because number [1] is incompatible with string [2]
                               // Error 2: Cannot call `action` because no more than 1 argument is expected by function [1]
  }
}

So there is a bug when combining import, typeof (?) and Component<Props>.

All 11 comments

Seems like flow is marking action as any. If you are importing action from a third-party module, in order to have flow mark it the correct type make sure you have the libdef for the module. Here is the link to the docs that provide instruction on how to that.
If you are importing action from a file within your project make sure that the file has flow initialized.

Maybe it was not clear, so here is another example with some comments:

// index.js
// @flow

// Let's import a function I've written in my project, with the '// @flow' annotation:

import { action } from '../state/actions'
// action: (param1: string) => {| type: string, param1: string |}
// This is the type inferred by Flow. I can see it in my editor.

// Now let's call it with some unexpected params:

action(1, 2, 3)
// Error 1: Cannot call `action` with `1` bound to `param1` because number [1] is incompatible with string [2]
// Error 2: Cannot call `action` because no more than 1 argument is expected by function [1]
// In this first case, Flow is catching all the errors as expected.

// Now let's pass 'action' to a component through its props:

const mapActionsToProps = {
  action
}

type Actions = typeof mapActionsToProps

type Props = {|
  ...Actions,
  otherProp: string, 
|}

class MyComponent extends Component<Props> {
  componentDidMount() {
    this.props.action(1, 2, 3)
    // In this case, no errors! Flow is not catching anything.
    // Yet it's the exact same function, passed through the Props
  }
}

connect(null, mapActionsToProps)(MyComponent)

So, either I'm not typing the Props correctly, either there is a bug somewhere.

I'd try doing:

type Props = {|
  ...$Exact<Actions>,
  otherProp: string, 
|}

@AlicanC Just tried and it doesn't fix it. Actually

const mapActionsToProps = {
  action
}

type Actions = typeof mapActionsToProps

already defines Actions as an exact object.

So I just discovered that it's actually because action is imported.

I was trying to reproduce the bug on flow.org/try but it works as expected when it is in the same file: https://flow.org/try/#0JYWwDg9gTgLgBAJQKYEMDGMA0cDecDCE4EAdkifAL5wBmURcA5FKhowFDtqkDO86MYKTgBeOAAowKKChABGAFxw+UYCQDmASlEA+CXhgBPMEiWMAgvgAqASQDyAOUbYpM+XEqbOAoSXFzsACZsAGYvdiMTOAAFejAeUVwAHzgAOnTzDF8EpMpONAAbFB4EgDEICDgkAA8YcgATBMJiMgoAHliIeL0cdjg4bhbyGAARYHqAWQgAVwpxbV7+-pgAC2AeVLA4jZ9SfyDQr368vK5efizeRMW4XZIlh8en55el9lPIpDhMwSuxT4gNFulxIPCAA

I also confirm that the type of the imported action is correctly inferred out of the component's props:

import { action } from '../state/actions'
// action: (param1: string) => {| type: string, param1: string |}

const mapActionsToProps = {
  action
}

type Actions = typeof mapActionsToProps

type Props = {| ...Actions |}

class MyComponent extends Component<Props> {
  componentDidMount() {
    action(1, 2, 3) // Error 1: Cannot call `action` with `1` bound to `param1` because number [1] is incompatible with string [2]
                    // Error 2: Cannot call `action` because no more than 1 argument is expected by function [1]

    this.props.action(1, 2, 3) // No errors!
  }
}

And if in the same file, it works as expected:

const action = (param1: string) => ({ type: 'ACTION', param1 })
// action: (param1: string) => {| type: string, param1: string |}

const mapActionsToProps = {
  action
}

type Actions = typeof mapActionsToProps

type Props = {| ...Actions |}

class MyComponent extends Component<Props> {
  componentDidMount() {
    action(1, 2, 3) // Error 1: Cannot call `action` with `1` bound to `param1` because number [1] is incompatible with string [2]
                    // Error 2: Cannot call `action` because no more than 1 argument is expected by function [1]

    this.props.action(1, 2, 3) // Error 1: Cannot call `action` with `1` bound to `param1` because number [1] is incompatible with string [2]
                               // Error 2: Cannot call `action` because no more than 1 argument is expected by function [1]
  }
}

So there is a bug when combining import, typeof (?) and Component<Props>.

@ggregoire have you found a workaround for this or what did you end up doing?

@kangax No I didn't find any workaround. I write the type of every action that I pass to my components…

type Props = {|
  action: (param1: string) => {| type: string, param1: string |},
  action2: () => void,
  // and so on…
|}

class MyComponent extends Component<Props> { ... }

@ggregoire I'm seeing a similar issue where props lose their types:

The shape seems to be determined properly:

screen shot 2018-10-16 at 12 55 42

But the 2nd level props are any:

screen shot 2018-10-16 at 12 53 05

I also checked with type-at-pos and it's consistent with what I'm seeing in VSCode:

➜  git:(master) ✗ flow type-at-pos app/scripts/xxx.jsx 10 27
{byUuid: {[string]: Array<Ticket>}, error: mixed, loaded: boolean, loading: boolean}

➜  git:(master) ✗ flow type-at-pos app/scripts/xxx.jsx 10 37
any

It's pretty bizarre how Flow recognizes this shape correctly but then reports property's value in the same expression as any. Perhaps some kind of internal shape caching?

@mroch any hints on what this could be?

@kangax I don't know if this is related to my issue. In your screenshot, zendeskTickets is not a "prop", it's just a nested object inside another object that you manually typed (state: GlobalState).

If you remove ...ReduxProps from Props, is state.zendeskTickets.loader still inferred as any? If yes then it's definitely not related to my issue.

@ggregoire I felt it might be the same issue because:

  • if I remove connect, loaded is determined correctly
  • if I remove spreading (type Props = ReduxProps) loaded is determined correctly

In my case, however, importing GlobalState or defining it locally doesn't make a difference.

Was this page helpful?
0 / 5 - 0 ratings