Skip to content

Typescript reducer’s switch case typeguard doesn’t work with object spread

I have a reducer that does different actions depending on the action.type, actions payload is different for certain actions.

    export enum ActionType {
      UpdateEntireState = "UPDATE_ENTIRE_STATE",
      UpdateStateItem = "UPDATE_STATE_ITEM"
    }
    
    type TypeEditData = {
      id: string;
      name: string;
      surname: string;
      age: number;
    };
    
    export type State = TypeEditData[];
    export type Action = UpdateEntireState | UpdateStateItem;
    
    type UpdateEntireState = {
      type: ActionType.UpdateEntireState;
      payload: State;
    };
    
    type UpdateStateItem = {
      type: ActionType.UpdateStateItem;
      payload: { id: string; data: TypeEditData };
    };
    
    export function reducer(state: State, action: Action): State {
      const { type, payload } = action;
    
      switch (type) {
        case ActionType.UpdateEntireState: {
          return [...payload];
        }
        case ActionType.UpdateStateItem: {
          const person = state.filter((item) => item.id === payload.id);
          return [...state, person[0]];
        }
        default: {
          throw Error("Wrong type of action!");
        }
      }
    }

This code won’t work, the errors will say that my action payload can be State or { id: string; data: TypeEditData }. However, if I access the payload property inside switch case using dot notation like so

return [...action.payload];

There won’t be any errors and the type guard will work fine. How const { type, payload } = action; differs from action.type and action.payload in terms of types and why doesn’t typeguard work with spread syntax?

TS version – 4.3.4

Answer

The issue is that you’ve defined payload before there was type information available on action, so it has the union type

State | {
    id: string;
    data: TypeEditData;
};

Define a local variable or simply use action.payload within each case statement and the compiler knows what type it has:

export function reducer(state: State, action: Action): State {
  // const { type, payload } = action;

  switch (action.type) {
    case ActionType.UpdateEntireState: {
      return [...action.payload];
    }
    case ActionType.UpdateStateItem: {
      const person = state.filter((item) => item.id === action.payload.id);
      return [...state, person[0]];
    }
    default: {
      throw Error("Wrong type of action!");
    }
  }
}

Variable type is established explicitly at declaration (e.g. const a: string) or implicitly at initialization (e.g. a = 4). Subsequent typeguard constructs are not used to re-evaluate the type of the variable. On the contrary, since the type of the variable is already defined at that point, that type is used to validate whether the later construct is valid for the variable.