Typescript cannot destruct union type

Viewed 1460

I have a union type Actions which is

type Actions = Readonly<{
    type: ActionTypes.LOAD_POST;
    payload: string;
}> | Readonly<{
    type: ActionTypes.LOAD_POST_FAIL;
    payload: string;
}> | Readonly<{
    type: ActionTypes.LOAD_POST_SUCCESS;
    payload: {
        url: string;
        post: Post;
    };
}>

(This is the generated type, the original was nested with multiple types and ReturnType.) ActionTypes is a string enum.

const postReducer = (state = initialPostState, action: Actions): PostState => {
  const { type, payload } = action;
  switch (action.type) {
    case ActionTypes.LOAD_POST_SUCCESS: {
      const { post } = action.payload; // No error
      return { ...state, loading: false, success: true, post };
    }
  }

  switch (type) {
    case ActionTypes.LOAD_POST: {
      return { ...state, loading: true };
    }
    case ActionTypes.LOAD_POST_SUCCESS: {
      // [ts] Type 'string | { url: string; post: IFullPost; }' has no property 'post' and no string index signature.
      const { post } = payload;
      return { ...state, loading: false, success: true, post };
    }
    case ActionTypes.LOAD_POST_FAIL: {
      return { ...state, loading: false, success: false, post: null };
    }
    default:
      return state;
  }
};

Why does the first one work but not the second one?

3 Answers

It's by design. Here is a very simplified example:

type Actions =
    {
        type: 1,
        payload: string;
    } |
    {
        type: 2,
        payload: { a: string }
    }

function r(action: Actions) {
    const { type } = action;
    switch (type) {
        case 2: {
            // Type 'string | { a: string; }' has no property 'a' and no string index signature.
            const { a } = action.payload;
        }

    }
}

When we destruct action object: const { type, payload } = action; we lose coupling information of destructed types. After this, type constant will have 1 | 2 type and payload will have string | { a: string; }, i.e. each type will union all possible options based on Actions type. This is why TS cannot figure out the exact type of payload, because in the switch condition we have absolutely separate variable.

You have to switch on action.type in order for the action.payload to change its type in the case statements.

You are experiencing TypeScript reaching the limits of its type inference by type guards.

In your non-working example, TypeScript can not infer anything about the already destructured variable payload, although it would be technically doable. I guess type guards only work on objects directly/literally involved in the guarding expression.

Related