Проблема
Redux Saga базируется на использовании генераторов (generator functions) для упрощённого управления побочными эффектами, таких как вызовы API, задержки и подписки на действия. Главный инструмент в Redux Saga — это эффекты, такие как call, put, takeLatest и другие.
Однако генераторы по своей природе сложны для типизации. В отличие от стандартных функций, генераторы работают итеративно, возвращая значения через yield. Это создаёт уникальный набор вызовов для типизации в TypeScript.
Типизация генераторов в TypeScript
Генераторы в JavaScript представляют собой функции, которые "замораживают" выполнение через yield. Вот простой пример генератора:
function* exampleGenerator() {
yield 'Hello';
return 'World';
}
const generator = exampleGenerator();
console.log(generator.next()); // { value: 'Hello', done: false }
console.log(generator.next()); // { value: 'World', done: true }
Теперь добавим типизацию:
function* exampleGenerator(): Generator<string, string, void> {
yield 'Hello';
return 'World';
}
Разбор Generator<T, TReturn, TNext>
T: тип значения, возвращаемого черезyield.TReturn: тип значения, возвращаемого с помощью командыreturn.TNext: тип значения, ожидаемого черезnext().
Пример с переданным значением
function* numberGenerator(): Generator<number, string, number> {
const input = yield 42; // Ожидаем число через next()
return `Received: ${input}`;
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 42, done: false }
console.log(gen.next(7)); // { value: 'Received: 7', done: true }
Типизация саг в Redux Saga
Саги в Redux Saga — это генераторы, которые взаимодействуют с эффектами, такими как call, put, takeEvery и др. Здесь типизация становится ещё более важной, так как мы часто работаем с параметрами, действиями и асинхронными вызовами.
Типизация эффекта call
call используется для вызова функций. Его типизация зависит от сигнатуры вызываемой функции:
import { call } from 'redux-saga/effects';
// Функция, которую будем вызывать
async function fetchData(id: number): Promise<string> {
return `Data for ID: ${id}`;
}
// Типизированный генератор с использованием call
function* fetchSaga() {
const data: string = yield call(fetchData, 5);
console.log(data); // Data for ID: 5
}
Типизация эффекта put
put используется для отправки действий. Здесь важно указать тип действия (action):
import { put } from 'redux-saga/effects';
interface MyAction {
type: string;
payload: number;
}
// Сага отправляет типизированное действие
function* sendActionSaga() {
const action: MyAction = { type: 'MY_ACTION', payload: 42 };
yield put(action);
}
Типизация takeEvery
takeEvery подписывается на типы действий и вызывает сага-функцию при их возникновении. Важно учитывать тип действия:
import { takeEvery, put } from 'redux-saga/effects';
interface MyAction {
type: 'MY_ACTION';
payload: number;
}
// Обработчик действия
function* handleAction(action: MyAction) {
console.log(`Payload received: ${action.payload}`);
yield put({ type: 'ACTION_HANDLED' });
}
// Типизированная подписка
function* watchActions() {
yield takeEvery<MyAction>('MY_ACTION', handleAction);
}
Практическое применение типизации Saga
Давайте расширим наше приложение и добавим асинхронные запросы через Redux Saga. Мы создадим приложение, которое будет получать данные с API и обрабатывать их.
Создаём срез slice состояния
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface DataState {
data: string[];
loading: boolean;
}
const initialState: DataState = {
data: [],
loading: false,
};
const dataSlice = createSlice({
name: 'data',
initialState,
reducers: {
fetchDataRequest(state) {
state.loading = true;
},
fetchDataSuccess(state, action: PayloadAction<string[]>) {
state.loading = false;
state.data = action.payload;
},
fetchDataFailure(state) {
state.loading = false;
},
},
});
export const { fetchDataRequest, fetchDataSuccess, fetchDataFailure } = dataSlice.actions;
export default dataSlice.reducer;
Сага для обработки запросов
import { call, put, takeEvery } from 'redux-saga/effects';
import { fetchDataRequest, fetchDataSuccess, fetchDataFailure } from './dataSlice';
async function fetchApi(): Promise<string[]> {
return ['React', 'Redux', 'TypeScript'];
}
function* fetchDataSaga() {
try {
const data: string[] = yield call(fetchApi); // Типизация call
yield put(fetchDataSuccess(data)); // Типизация put
} catch (error) {
yield put(fetchDataFailure());
}
}
export function* watchFetchData() {
yield takeEvery(fetchDataRequest.type, fetchDataSaga);
}
Интеграция Saga в Store
import createSagaMiddleware from 'redux-saga';
import { configureStore } from '@reduxjs/toolkit';
import dataReducer from './dataSlice';
import { watchFetchData } from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
reducer: {
data: dataReducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(sagaMiddleware),
});
sagaMiddleware.run(watchFetchData);
export default store;
Полезные советы и особенности
Работая с TypeScript и Redux Saga, вы можете столкнуться с несколькими особенностями:
callиputвозвращают значения черезyield. Помните об этом, чтобы правильно указывать типы.- Ошибки в саге часто сложно отлаживать без настройки try/catch. Добавляйте типизацию для обработки ошибок.
- Используйте интерфейсы для типизации действий. Это особенно важно для методов вроде
takeEveryилиtakeLatest, так как они обрабатывают действия.
Типизация генераторов и саг делает код более безопасным и понятным. Теперь вы сможете эффективно управлять сложными асинхронными процессами и минимизировать ошибки в Redux-приложениях!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ