Reputation: 1344
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
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