Jstuff
Jstuff

Reputation: 1344

Typing reducer with redux-actions library

I'm struggling to type the second argument, action, of my reducer now that I use createAction from the redux-actions library.

Part of my reducer code is as follows

type AddItems = 'addItems';
const AddItems: AddItems = 'addItems';

interface AddItemAction extends Action<CartItem> {
    type: AddItems;
}

export const addItems = createAction<CartItem>(AddItems);

type RemoveItem = 'removeItem';
const RemoveItem: RemoveItem = 'removeItem';

interface RemoveItemAction extends Action<number> {
    type: RemoveItem;
}

export const removeItem = createAction<number>(RemoveItem);

export type ShoppingCartActions = AddItemAction | RemoveItemAction;

export const shoppingCartActions = {
    addItems,
    removeItem
};

export default function shoppingCartReducer(state: ShoppingCartStore = shoppingCartInitialState, action: ShoppingCartActions) {
    const { type, payload } = action;    
    switch (type) {
        case AddItems:
           return {
            cartItems: [
                ...state.cartItems.filter(cartItem => cartItem.item.id !== payload!.item.id),
                payload
            ]               
           };
        case RemoveItem:
           return {
               cartItems: [
                   ...state.cartItems.filter(cartItem => cartItem.item.id !== payload)
               ]
           };
        default:
            return state;
    }
}

The problem I have is that when I access a property on payload in my reducer it thinks that the property must exist for each case. For example, in the case of addItems I access payload.item but typescript complains because it also looks for item to be on the type number coming from RemoveItemAction.

Upvotes: 0

Views: 309

Answers (1)

jcalz
jcalz

Reputation: 327624

Disclaimer: I don't have the redux or redux-actions libaries installed locally so I can't be 100% sure if this answer is correct; please test it.


ShoppingCartActions is a discriminated union where type is the discriminant property. You are expecting TypeScript to act as advertised when you check the discriminant: it should automatically narrow the type of the action variable to the appropriate consituent of the union.

Unfortunately, you are destructuring action into two variables, type and payload like this:

const { type, payload } = action;    

and the compiler does not understand that the types of type and payload are correlated. That is, if you narrow type from AddItems | RemoveItem to AddItems, the compiler does not automatically narrow payload from CartItem | number to CartItem. Even though we know that this must be the case, the compiler is not as smart as us, and the heuristics it uses to analyze the control flow are not up to the task of understanding "entangled variables" like this.

You are not the first person to run into this issue. The comments by the language designers in those issues are generally something like "it would be nice if the compiler could do this for you, but it would be very expensive to implement and lead to poor compiler performance".


The easiest fix in this case is to use discriminated unions the way that TypeScript intends: do not destructure action. Use direct property accesses on action instead:

export default function shoppingCartReducer(
  state: ShoppingCartStore = shoppingCartInitialState, 
  action: ShoppingCartActions
) {
  // switch on property of action
  switch (action.type) {
    case AddItems:
      return {
        // action.payload is now CartItem
        cartItems: [              
          ...state.cartItems.filter(
            cartItem => cartItem.item.id !== action.payload!.item.id
          ),
          action.payload
        ]
      };
    case RemoveItem:
      // action.payload is now number
      return {
        cartItems: [
          ...state.cartItems.filter(
            cartItem => cartItem.item.id !== action.payload
          )
        ]
      };
    default:
      return state;
  }
}

This should now work as intended, I think.


Hope that helps. Good luck!

Upvotes: 2

Related Questions