Короткий ответ: handleActions обычно относится к библиотеке redux-actions, а не к современному Redux Toolkit. Если проект legacy и handleActions уже используется, типизируйте состояние, payload и action creator явно. Если проект новый, лучше рассмотреть createSlice из Redux Toolkit: там TypeScript-поддержка проще и надежнее
Ниже пример именно для случая, когда handleActions уже есть в проекте
Тип состояния
type CounterState = {
value: number;
loading: boolean;
};
const initialState: CounterState = {
value: 0,
loading: false,
};
Тип payload
type IncrementPayload = {
amount: number;
};
Если action несет данные, не оставляйте payload как any. Именно payload чаще всего ломает типизацию reducer
Action creator
В legacy-коде может быть так:
import { createAction } from "redux-actions";
export const increment = createAction<IncrementPayload>("counter/increment");
Использование:
dispatch(increment({ amount: 2 }));
TypeScript должен ругаться, если передать { amount: "2" }
Reducer через handleActions
import { handleActions, Action } from "redux-actions";
export const counterReducer = handleActions<CounterState, IncrementPayload>(
{
[increment.toString()]: (
state,
action: Action<IncrementPayload>
): CounterState => {
return {
...state,
value: state.value + (action.payload?.amount ?? 0),
};
},
},
initialState
);
Здесь явно указаны:
- тип состояния
CounterState; - тип payload
IncrementPayload; - тип action
Action<IncrementPayload>; - возвращаемый тип reducer.
Почему payload может быть optional
В некоторых типах redux-actions payload может считаться необязательным. Поэтому в примере используется:
action.payload?.amount ?? 0
Если по бизнес-логике payload обязателен, можно добавить проверку и выбросить ошибку:
if (!action.payload) {
return state;
}
Как типизировать несколько action
Если reducer обрабатывает несколько действий, для каждого payload лучше завести отдельный тип:
type SetLoadingPayload = {
loading: boolean;
};
export const setLoading = createAction<SetLoadingPayload>("counter/setLoading");
[setLoading.toString()]: (
state,
action: Action<SetLoadingPayload>
): CounterState => ({
...state,
loading: action.payload?.loading ?? state.loading,
})
Не пытайтесь сделать один общий payload “на все случаи”. Так типы быстро станут расплывчатыми, а reducer начнет принимать поля, которые конкретному action не нужны
Современная альтернатива через Redux Toolkit
Для нового кода проще:
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
type CounterState = {
value: number;
};
const initialState: CounterState = { value: 0 };
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state, action: PayloadAction<{ amount: number }>) {
state.value += action.payload.amount;
},
},
});
Redux Toolkit сейчас является рекомендуемым путем для обычных Redux-проектов. handleActions стоит поддерживать, если он уже есть в кодовой базе
Частые ошибки
Первая ошибка — типизировать только state, но оставить action как any. Тогда TypeScript не проверяет payload
Вторая ошибка — использовать строку action type вручную в нескольких местах. Лучше брать increment.toString() или единый constant
Третья ошибка — смешивать старый redux-actions и Redux Toolkit без причины. Выберите стиль для конкретного модуля
Четвертая ошибка — не проверять optional payload, если тип библиотеки допускает его отсутствие
Пятая ошибка — продолжать наращивать legacy-слой, когда модуль уже можно спокойно переписать на createSlice. Для старого проекта это не всегда срочно, но для нового кода лучше не начинать с handleActions
Самопроверка
Создайте action increment({ amount: 2 }), reducer через handleActions и dispatch с неправильным payload { amount: "2" }. TypeScript должен поймать ошибку. Затем перепишите тот же пример на createSlice и сравните объем кода
Что почитать дальше по TypeScript
Если нужен общий маршрут по теме, откройте рубрику TypeScript. Для соседних задач пригодятся эти разборы:
- 10 вопросов по TypeScript и ответы на них
- Any, unknown, never и strict
- App.tsx: что это за файл и как добавить TypeScript в React
- Conditional types в TypeScript: как работает extends



