Типизация useReducer
Базовая типизация
type State = {
count: number;
};
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'set':
return { count: action.payload };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
TypeScript выводит State и Action из сигнатуры редьюсера.
Явное указание
const [state, dispatch] = useReducer<typeof reducer>(reducer, { count: 0 });
Обычно не нужно.
Dispatch
import type { Dispatch } from 'react';
type DispatchType = Dispatch<Action>;
Dispatch<Action> принимает только допустимые действия:
dispatch({ type: 'increment' }); // ок
dispatch({ type: 'set', payload: 5 }); // ок
dispatch({ type: 'invalid' }); // ошибка
dispatch({ type: 'set' }); // ошибка: нет payload
Ленивая инициализация
type State = { count: number };
function init(initialCount: number): State {
return { count: initialCount };
}
const [state, dispatch] = useReducer(reducer, 0, init);
init получает второй аргумент useReducer и возвращает начальное состояние.
Generic-reducer
function createReducer<S, A extends { type: string }>() {
return (state: S, action: A): S => {
return state;
};
}
Action creators
const actions = {
increment: (): Action => ({ type: 'increment' }),
decrement: (): Action => ({ type: 'decrement' }),
set: (payload: number): Action => ({ type: 'set', payload }),
};
dispatch(actions.set(5));
Типизация через discriminated union
Самый распространённый способ — каждый action — отдельный тип с дискриминатором:
type Action =
| { type: 'add'; item: Item }
| { type: 'remove'; id: number }
| { type: 'update'; id: number; patch: Partial<Item> }
| { type: 'clear' };
function reducer(state: Item[], action: Action): Item[] {
switch (action.type) {
case 'add':
return [...state, action.item];
case 'remove':
return state.filter((i) => i.id !== action.id);
case 'update':
return state.map((i) => (i.id === action.id ? { ...i, ...action.patch } : i));
case 'clear':
return [];
}
}
В каждом case доступны только релевантные поля.
Exhaustive check
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'set': return { count: action.payload };
default:
const _exhaustive: never = action;
return state;
}
}
Если добавишь новый тип action и забудешь обработать — TypeScript ошибётся.
Контекст с reducer
type State = { count: number };
type Action = { type: 'increment' } | { type: 'decrement' };
const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch<Action> | null>(null);
Итоги
- Типы выводятся из редьюсера
Dispatch<Action>— тип диспатча- Discriminated union — основной паттерн
neverдля exhaustive check- Ленивая инициализация — третий аргумент
- Action creators дают удобство