Joshua
Joshua

Reputation: 6241

Typescript cannot destruct union type

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?

Upvotes: 3

Views: 1806

Answers (3)

Hero Wanders
Hero Wanders

Reputation: 3292

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.

Upvotes: 0

shokha
shokha

Reputation: 3179

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.

Upvotes: 3

basarat
basarat

Reputation: 276115

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

Upvotes: 0

Related Questions