Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Як типізувати useReducer, щоб кожна дія мала правильний набір полів?

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 });

Що дає типізація:

  • у кожній гілці switch TypeScript звужує тип: у 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).

Докладніше в документації: TypeScript у React: useReducer

Схожі питання