Flow: Flow errors when using union types

Created on 4 Mar 2017  路  5Comments  路  Source: facebook/flow

I have been trying to use union types when working with redux, but I am getting an error.

Accessing action.payload.id inside the state.map() call causes an error, however destructing outside of the map doesn't cause an error.

Example flow link.

/* @flow */

type NotificationsType = Array<{id: string, show: boolean}>;

type ActionType = 
  { type: 'SUCCESS_NOTIFICATION', +payload: { +message: string } } |
  { type: 'HIDE_NOTIFICATION', +payload: { +id: string } } |
  { type: 'ANOTHER_THING' }
;

export default function reducer(state: NotificationsType = [], action: ActionType): NotificationsType {
    switch (action.type) {
        case 'SUCCESS_NOTIFICATION': 
            return [...state, { id: 'FOOBAR', show: true }] 

        case 'HIDE_NOTIFICATION': {
            const { payload } = action;
            const { id } = payload;

            return state.map((notification) => {
                if (action.payload.id !== notification.id) {
                    return notification;
                }

                if (id !== notification.id) {
                    return notification;
                }

                return { ...notification, show: false };
            });
        }

        default:
            return state;
    }
}

Flow 0.41.0 errors with.

21:                 if (action.payload.id !== notification.id) {
                              ^ property `payload`. Property not found in
21:                 if (action.payload.id !== notification.id) {
                       ^ object type

Most helpful comment

The problem here is that whenever you do a function call, Flow removes all refinements to the types. So as soon as you call .map Flow reverts back to the unrefined type for action.

This is because Objects are mutable in Javascript and any function call may have changed the value of the Object.

However, since you marked you actions as covariant, maybe the type refinements should work differently.


As of right now, you've got the right idea, which is to store your refined types in variables and use those instead.

All 5 comments

The problem here is that whenever you do a function call, Flow removes all refinements to the types. So as soon as you call .map Flow reverts back to the unrefined type for action.

This is because Objects are mutable in Javascript and any function call may have changed the value of the Object.

However, since you marked you actions as covariant, maybe the type refinements should work differently.


As of right now, you've got the right idea, which is to store your refined types in variables and use those instead.

Tags: flow union type refinement bug (for anyone googling)

I've stumbled into the same problem, example code:

Flow version 0.48.0

// @flow

type Diagram = {
  name: string,
  dateCreated: number,
  dateModified: ?number,
  xml: string,
  id: string,
};

type Diagrams = Array<Diagram>;

type DiagramsState = {
  diagrams: Diagrams,
};

type DiagramsAction =
  | {|
      +type: 'SET_DIAGRAMS',
      +payload: Diagrams,
    |}
  | {|
      +type: 'ADD_DIAGRAM',
      +payload: Diagram,
    |}
  | {|
      +type: 'EDIT_DIAGRAM',
      +payload: Diagram,
    |};

const diagrams = (state: Diagrams, action: DiagramsAction): Diagrams => {
  switch (action.type) {
    case 'SET_DIAGRAMS':
      return action.payload;

    case 'ADD_DIAGRAM':
      return [...state, action.payload];

    case 'EDIT_DIAGRAM':
      return state.map(diagram => {
        return diagram.id === action.payload.id ? action.payload : diagram;
      });

    default:
      return state;
  }
};
41:         return diagram.id === action.payload.id ? action.payload : diagram;
                                                 ^ property `id`. Property not found in
41:         return diagram.id === action.payload.id ? action.payload : diagram;
                                  ^ Array
41:         return diagram.id === action.payload.id ? action.payload : diagram;
                                                      ^ array type. This type is incompatible with
11: type Diagrams = Array<Diagram>;
                          ^ object type

https://flow.org/try/#0PTAEAEDMBsHsHcBQiAuBPADgU1AEQJYCGA5gE6EC2oAvKAN6KigB2lWAXKAM4qn7PEANI1AATQiiwBhUlglZRnZgFcKAIyylhTcZICysUfkj4FnAPwr1m7aAAeFaJx58Bt-Iu69+QxAF8AbmR0bDwiMkouGlAAQVJyNAAeAhJyCgA+INRMHBSIii4AZRR5aIYdcLSuTjyq4UDgnLDUyJiAYxR8WGYaEQAfej6RJiYAahCOUAByQoBRABUAfVwASRiAcQAlGL1CqdsR0FGMQjQ4Qk9ayIPQPr9+weGR8ZzOKZjcXGW1rZ39p7GJzOsAuNUqlBudwedCGhzGEzes1WS1WG22en+cKOQPOl3BFEhDUQbW6PDE+KitAAFDx5GCWgVBKBCB0usx6fkuO1Ot0AJQcqo0dL0ERceD4FBtAAWoCpLJ5zAAdBNeSLDm1CFwcDMFt80Ts9uwAaBZChlKQevK2YqcSDRFl1ZrtR8vqjfhijVjTeaegBtRUB2mSJlW7o2064gC6DpGGq10yRKxRP3RU09cO9Fq88kVFEIGCpRgZQrVWJNWDNWaL+UVHho1FooaVtoutdEoHMzNZYZb7c41bSQSxfl5MZ0WEghGU0BQ6cOmZ6QawQ9A90CQA

This problem can be circumvented by copying the action, example code:

// @flow

type Diagram = {
  name: string,
  dateCreated: number,
  dateModified: ?number,
  xml: string,
  id: string,
};

type Diagrams = Array<Diagram>;

type DiagramsState = {
  diagrams: Diagrams,
};

type DiagramsAction =
  | {|
      +type: 'SET_DIAGRAMS',
      +payload: Diagrams,
    |}
  | {|
      +type: 'ADD_DIAGRAM',
      +payload: Diagram,
    |}
  | {|
      +type: 'EDIT_DIAGRAM',
      +payload: Diagram,
    |};

const diagrams = (state: Diagrams, action: DiagramsAction): Diagrams => {
  switch (action.type) {
    case 'SET_DIAGRAMS':
      return action.payload;

    case 'ADD_DIAGRAM':
      return [...state, action.payload];

    case 'EDIT_DIAGRAM':
      const actionCopy = action;
      return state.map(diagram => {
        return diagram.id === actionCopy.payload.id ? actionCopy.payload : diagram;
      });

    default:
      return state;
  }
};

https://flow.org/try/#0PTAEAEDMBsHsHcBQiAuBPADgU1AEQJYCGA5gE6EC2oAvKAN6KigB2lWAXKAM4qn7PEANI1AATQiiwBhUlglZRnZgFcKAIyylhTcZICysUfkj4FnAPwr1m7aAAeFaJx58Bt-Iu69+QxAF8AbmR0bDwiMkouGlAAQVJyNAAeAhJyCgA+INRMHBSIii4AZRR5aIYdcLSuTjyq4UDgnLDUyJiAYxR8WGYaEQAfej6RJiYAahCOUAByQoBRABUAfVwASRiAcQAlGL1CqdsR0FGMQjQ4Qk9ayIPQPr9+weGR8ZzOKZjcXGW1rZ39p7GJzOsAuNUqlBudwedCGhzGEzes1WS1WG22en+cKOQPOl3BFEhDUQbW6PDE+KitAAFDx5GCWgVBKBCB0usx6fkuO1Ot0AJQcqo0dL0ERceD4FBtAAWoCpLJ5zAAdBNeSLDm1CFwcDMFt80Ts9uwAaBZChlKQevK2YqcSDRFl1ZrtR8vqjfhijVjTeaegBtRUB2mSJlW7o2064gC6DpGGq10yRKxRP3RU09cJJzDJoeYUlgGDQ0RzQS9WDNFq88kVFEIGCpRgZQrVWJNZZ95IZio8NGotBzeYL4eBFy7olA5mZrO6A7QQ9xoE4DfyJbhfl5MZ0WEghGU0BQ6cO3orQawK-ugSAA

Moreover; the actionCopy variable can be declared with both const or let and it's still fixed (mutability not an issue)

I'm having the exact same problem. Sample code:

type PERMISSIONS_ADDED_TYPE = {
  type: 'PERMISSIONS_ADDED',
  payload: Array<string>
}
type PERMISSION_REMOVED_TYPE = {
  type: 'PERMISSION_REMOVED',
  payload: string
}
type PERMISSIONS_CLEARED_TYPE = {
  type: 'PERMISSION_REMOVED'
}
type PERMISSION_ACTION_TYPE =
  | PERMISSIONS_ADDED_TYPE
  | PERMISSION_REMOVED_TYPE
  | PERMISSIONS_CLEARED_TYPE


type State = Array<string>;

const permissions = (state: State = [], action: PERMISSION_ACTION_TYPE): State => {
  const { type, payload } = action;
  switch(type) {
    case types.PERMISSIONS_ADDED: {
      if (typeof payload !== 'undefined') {
        return [...state, ...payload];
      }

      return state;
    }
    case types.PERMISSIONS_CLEARED: {
      return [];
    }
    case types.PERMISSION_REMOVED: {
      return state.filter(
        cpermission => cpermission !== payload
      );
    }
    default:
      return state;
  }
}

And got the following error:
Error: src/scripts/reducers/permissions.js:8 8: const { type, payload } = action; ^^^^^^^ propertypayload. Property not found in 8: const { type, payload } = action; ^^^^^^ object type

To circumvent this issue I replaced type PERMISSIONS_CLEARED for the following:

type PERMISSIONS_CLEARED_TYPE = {
  type: 'PERMISSION_REMOVED',
  payload?: Object
}

But I feel this is not ideal. I thought union type would take care of this.

I think that in your case the error is legit. You're attempting to access the payload attribute of a type that potentially doesn't have that attribute, _before_ the refinement.

Also, I found that most type errors I get come from any function/method call, which apparently makes flow very suspicious and requires more comforting assurances.

Try this refactor:

type PERMISSIONS_ADDED_TYPE = {
  type: 'PERMISSIONS_ADDED',
  payload: ?Array<string>
}

type PERMISSION_REMOVED_TYPE = {
  type: 'PERMISSION_REMOVED',
  payload: string
}

type PERMISSIONS_CLEARED_TYPE = {
  type: 'PERMISSION_REMOVED'
}

type PERMISSION_ACTION_TYPE =
  | PERMISSIONS_ADDED_TYPE
  | PERMISSION_REMOVED_TYPE
  | PERMISSIONS_CLEARED_TYPE


type State = Array<string>;

const permissions = (state: State = [], action: PERMISSION_ACTION_TYPE): State => {
  switch(action.type) {
    case types.PERMISSIONS_ADDED: {
      if (typeof payload !== 'undefined') {
        return [...state, ...action.payload];
      }
      return state;
    }

    case types.PERMISSIONS_CLEARED: {
      return [];
    }

    case types.PERMISSION_REMOVED: {
      const { payload } = action;
      return state.filter(
        cpermission => cpermission !== payload
      );
    }

    default:
      return state;
  }
}

@DrummerHead thanks! that was a great explanation! I didn't even know about the _refinement_ concept. Thank you again.

Was this page helpful?
0 / 5 - 0 ratings