useReducer найкраще поєднується з дискримінованим union дій: поле type визначає, які ще поля має дія.
type CartItem = { id: number; title: string; price: number; qty: number };
type CartState = {
items: CartItem[];
coupon: string | null;
};
type CartAction =
| { type: 'added'; item: CartItem }
| { type: 'removed'; id: number }
| { type: 'quantityChanged'; id: number; qty: number }
| { type: 'couponApplied'; code: string }
| { type: 'cleared' };
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'added':
return { ...state, items: [...state.items, action.item] };
case 'removed':
return { ...state, items: state.items.filter((i) => i.id !== action.id) };
case 'quantityChanged':
return {
...state,
items: state.items.map((i) => (i.id === action.id ? { ...i, qty: action.qty } : i)),
};
case 'couponApplied':
return { ...state, coupon: action.code };
case 'cleared':
return { items: [], coupon: null };
default: {
const unreachable: never = action;
return state;
}
}
}
const [cart, dispatch] = useReducer(cartReducer, { items: [], coupon: null });
Що дає типізація:
- у кожній гілці
switchTypeScript звужує тип: уcase 'removed'доступнеaction.id, аaction.item- помилка; dispatchперевіряє дії:dispatch({ type: 'removed' })безidчи з неіснуючимtypeне скомпілюється;- перевірка повноти: присвоєння
neverуdefaultдає помилку компіляції, якщо додати новий тип дії й забути обробити його в редьюсері.
Тип стану й дій виводиться з функції-редьюсера, тож параметри useReducer вказувати явно не треба. Явна анотація повернення CartState у редьюсері ловить випадки, коли гілка повертає щось не те.
Назви дій - у минулому часі («що сталося»: added, couponApplied), як радить документація React, а не команди (ADD_ITEM).
Лінива ініціалізація - третій аргумент: useReducer(cartReducer, userId, createInitialCart), де createInitialCart(userId) повертає початковий стан. Тип аргументу перевіряється.
Пастка: мутувати стан у редьюсері (state.items.push(...)) TypeScript не заборонить - для гарантій незмінності типи можна оголосити як readonly (readonly CartItem[]), або використати Immer (useImmerReducer).