Flow: intersection types and possibly undefined errors causing additional, invalid missing prop errors

Created on 2 Aug 2016  路  4Comments  路  Source: facebook/flow

Flow 0.30.0

/* @flow */

import React, {Component} from 'react'
import * as Immutable from 'immutable'

export type Props = {
  calibration: Immutable.Map<any, any>,
}

type PointProps = Props & {
  pointIndex: number,
}

export class Point extends Component<void, PointProps, void> {
  static validate(props: PointProps): Object {
    const {pointIndex, calibration} = props
    const outputValue = calibration.getIn(['points', pointIndex, 'y'])
    return {outputValue: {error: `'${outputValue}' is not a valid number`}, valid: false}
  };
  render() {
    return null
  }
}

Errors:

react-sandbox/scratch/flowissues.js:16
 16:     const {pointIndex, calibration} = props
                            ^^^^^^^^^^^ property `calibration`. Property cannot be accessed on any member of intersection type
 16:     const {pointIndex, calibration} = props
                                           ^^^^^ intersection
  Member 1:
   10: type PointProps = Props & {
                         ^^^^^ Props
  Error:
   18:     return {outputValue: {error: `'${outputValue}' is not a valid number`}, valid: false}
                                            ^^^^^^^^^^^ undefined (too few arguments, expected default/rest parameters). This type cannot be added to
   18:     return {outputValue: {error: `'${outputValue}' is not a valid number`}, valid: false}
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ string
  Member 2:
   10: type PointProps = Props & {
                                 ^ object type
  Error:
   16:     const {pointIndex, calibration} = props
                              ^^^^^^^^^^^ property `calibration`. Property not found in
   10: type PointProps = Props & {
                                 ^ object type

This only seems to hapeen if PointProps is an intersection type. The errors about calibration not being part of the shape are wrong and go away if the string template error is fixed.

I can also repro this way:

/* @flow */

import React, {Component} from 'react'
import * as Immutable from 'immutable'

export type Props = {
  calibration: Immutable.Map<any, any>,
}

type PointProps = Props & {
  pointIndex: number,
}

export class Point extends Component<void, PointProps, void> {
  static validate(props: PointProps): Object {
    const {pointIndex, calibration} = props
    const outputValue = calibration.getIn(['points', pointIndex, 'y'])
    return outputValue.toFixed(1)
  };
  render() {
    return null
  }
}

Errors:

react-sandbox/scratch/flowissues.js:16
 16:     const {pointIndex, calibration} = props
                            ^^^^^^^^^^^ property `calibration`. Property cannot be accessed on any member of intersection type
 16:     const {pointIndex, calibration} = props
                                           ^^^^^ intersection
  Member 1:
   10: type PointProps = Props & {
                         ^^^^^ Props
  Error:
   18:     return outputValue.toFixed(1)
                  ^^^^^^^^^^^^^^^^^^^^^^ call of method `toFixed`. Method cannot be called on possibly undefined value
   18:     return outputValue.toFixed(1)
                  ^^^^^^^^^^^ undefined (too few arguments, expected default/rest parameters)
  Member 2:
   10: type PointProps = Props & {
                                 ^ object type
  Error:
   16:     const {pointIndex, calibration} = props
                              ^^^^^^^^^^^ property `calibration`. Property not found in
   10: type PointProps = Props & {
                                 ^ object type
bug declarations

Most helpful comment

What you're saying made sense at first but on more thought it doesn't. I can see how for a _union_ type, A | B, if the A branch fails, then it should retry the code assuming the value is of type B. But with an intersection type A & B, if something fails according to the conditions of A, it's not right to retry assuming the value is just of type B, because the value must also be of type A, right?

The more I think about this the hazier it seems to me. Obviously for union types flow just needs to check if one branch of the union passes. So at first I would think that with intersection types, all branches have to pass -- but of course the branch lacking calibration would never pass -- so I'm guessing flow does something more complicated for shape intersections, like combining the props present?

In any case no value of type Props & { calibration: ... } can lack a calibration field, so isn't it a bug if a branch of flow checking believes it doesn't have a calibration?

All 4 comments

So this is a bug in the immutable.js.flow file you're probably using. See if changing these two lines:
https://github.com/facebook/immutable-js/blob/master/type-definitions/immutable.js.flow#L55-L56

to this single line:

getIn<T>(searchKeyPath: ESIterable<any>, notSetValue?: T): T;

helps.

Detailed explanation: when you don't provide a notSetValue arg in your call, you're implicitly passing undefined, so Flow selects the first case with T = void.

@avikchaudhuri I think you're missing the point. The question is why the incorrect errors like this were getting triggered:

 16:     const {pointIndex, calibration} = props
                            ^^^^^^^^^^^ property `calibration`. Property cannot be accessed on any member of intersection type
 16:     const {pointIndex, calibration} = props
                                           ^^^^^ intersection

On the surface these seem to have nothing to do with immutable.js, yet when I try to repro with calibration: {outputValue?: number} I only get the string error.

OK, reopening to see how we can do this better. In general, when one branch of an intersection fails for whatever reason, we have to try the other branch. In this case the second branch is doomed to fail, and the first fails because of a declaration bug. I agree it doesn't match expectations, since it's clear in our heads that the second branch should never succeed. But to Flow the two branches are equally to blame.

What you're saying made sense at first but on more thought it doesn't. I can see how for a _union_ type, A | B, if the A branch fails, then it should retry the code assuming the value is of type B. But with an intersection type A & B, if something fails according to the conditions of A, it's not right to retry assuming the value is just of type B, because the value must also be of type A, right?

The more I think about this the hazier it seems to me. Obviously for union types flow just needs to check if one branch of the union passes. So at first I would think that with intersection types, all branches have to pass -- but of course the branch lacking calibration would never pass -- so I'm guessing flow does something more complicated for shape intersections, like combining the props present?

In any case no value of type Props & { calibration: ... } can lack a calibration field, so isn't it a bug if a branch of flow checking believes it doesn't have a calibration?

Was this page helpful?
0 / 5 - 0 ratings