51. TypeScript с Redux: основы
TypeScript: Redux — Основы Строгой Типизации
Заголовок раздела «TypeScript: Redux — Основы Строгой Типизации»Привет, кодер! Сегодня мы погрузимся в одну из самых мощных комбинаций в мире фронтенда: Redux и TypeScript. Если Redux — это мозг вашего приложения, управляющий центральной нервной системой (состоянием), то TypeScript — это личный нейрохирург, который гарантирует, что каждый сигнал, каждый нейрон и каждый процесс работают безупречно, без неожиданных сбоев.
Вы уже знакомы с Redux и его основными принципами: хранилище, редюсеры, действия. Но когда проект начинает расти, и ваше состояние становится похожим на сложный лабиринт Минотавра, именно TypeScript становится тем Ариадниным клубком, который не даст вам заблудиться, обеспечивая строгую типизацию на каждом шагу. Погнали!
🚀 Зачем TypeScript в Redux?
Заголовок раздела «🚀 Зачем TypeScript в Redux?»Без TypeScript Redux-приложения часто сталкиваются с проблемами типа “ой, я ожидал строку, а получил число” или “почему у этого объекта нет свойства userId?”. В больших командах или долгоживущих проектах это может привести к кошмару отладки. TypeScript решает эти проблемы, позволяя:
- Объявлять четкую структуру состояния (State): Больше никаких догадок о том, что хранится в
store.user.profile. - Типизировать действия (Actions): Гарантировать, что каждое действие имеет ожидаемый
typeи корректнуюpayload. - Безопасно работать с редюсерами (Reducers): Компилятор проследит, чтобы редюсеры всегда принимали и возвращали состояние правильного типа, и корректно обрабатывали типизированные действия.
- Улучшать автодополнение и рефакторинг: IDE становится вашим лучшим другом, подсказывая доступные поля и методы, а изменение типа в одном месте безопасно отражается по всему приложению.
В общем, TypeScript превращает Redux из мощного, но потенциально опасного инструмента в предсказуемую, безопасную и невероятно эффективную систему управления состоянием.
🛠️ Типизируем State
Заголовок раздела «🛠️ Типизируем State»Начнем с самого фундамента: состояния. Все ваше приложение строится на данных, хранящихся в Redux store. Определить тип RootState — это как нарисовать архитектурный план здания.
export interface UserProfile { id: string; username: string; email: string; isAuthenticated: boolean; preferences: { theme: 'dark' | 'light'; notificationsEnabled: boolean; };}
export interface Product { id: string; name: string; price: number; currency: string; quantity: number;}
export interface AppState { user: UserProfile; products: Product[]; isLoading: boolean; error: string | null;}
// Этот тип будет представлять весь глобальный стейт приложения.// Мы получим его позже из самого стора.// export type RootState = ReturnType<typeof rootReducer>;📝 Типизируем Actions
Заголовок раздела «📝 Типизируем Actions»Действия — это команды, которые говорят Redux, что произошло. Крайне важно, чтобы каждая команда была четкой и имела строго определенные параметры. Здесь мы часто используем объединяющие (union) типы для всех возможных действий.
// Определяем базовый интерфейс для всех действий Reduxinterface Action<T extends string> { type: T;}
// Интерфейс для действия входа пользователяexport interface LoginAction extends Action<'user/login'> { payload: { username: string; token: string; };}
// Интерфейс для действия выхода пользователяexport interface LogoutAction extends Action<'user/logout'> {}
// Интерфейс для действия обновления профиляexport interface UpdateProfileAction extends Action<'user/updateProfile'> { payload: Partial<{ username: string; email: string; theme: 'dark' | 'light'; }>;}
// Объединяем все действия пользователя в один типexport type UserAction = LoginAction | LogoutAction | UpdateProfileAction;
// Пример action creatorexport const login = (username: string, token: string): LoginAction => ({ type: 'user/login', payload: { username, token },});
export const logout = (): LogoutAction => ({ type: 'user/logout',});
export const updateProfile = ( updates: Partial<{ username: string; email: string; theme: 'dark' | 'light' }>): UpdateProfileAction => ({ type: 'user/updateProfile', payload: updates,});Обратите внимание на Action<'user/login'>. Это позволяет TypeScript понимать, что type всегда будет строковым литералом 'user/login', а не просто string.
🛡️ Типизируем Reducers
Заголовок раздела «🛡️ Типизируем Reducers»Редюсеры — это чистые функции, которые принимают текущее состояние и действие, а затем возвращают новое состояние. Здесь TypeScript следит за тем, чтобы входные и выходные типы соответствовали AppState и чтобы логика обработки действий была безопасной.
import { UserProfile } from '../types';import { UserAction } from './actions'; // Импортируем наш тип действий
// Начальное состояние для пользователяconst initialUserState: UserProfile = { id: 'guest', username: 'Гость', email: '', isAuthenticated: false, preferences: { theme: 'light', notificationsEnabled: true, },};
// Типизируем параметры и возвращаемое значение редюсераexport const userReducer = ( state: UserProfile = initialUserState, // Указываем тип для состояния и начальное значение action: UserAction // Указываем тип для действия): UserProfile => { switch (action.type) { case 'user/login': // TypeScript знает, что action здесь типа LoginAction, // поэтому payload.username и payload.token доступны return { ...state, id: action.payload.token.split('-')[0], // Просто пример id из токена username: action.payload.username, isAuthenticated: true, }; case 'user/logout': // TypeScript знает, что action здесь типа LogoutAction return { ...initialUserState, // Сбрасываем до начального состояния id: 'guest', // Убедимся, что ID тоже сброшен }; case 'user/updateProfile': // TypeScript знает, что action здесь типа UpdateProfileAction return { ...state, username: action.payload.username || state.username, email: action.payload.email || state.email, preferences: { ...state.preferences, theme: action.payload.theme || state.preferences.theme, }, }; default: // Всегда возвращаем текущее состояние по умолчанию return state; }};🔗 Типизируем Store и Dispatch
Заголовок раздела «🔗 Типизируем Store и Dispatch»И, наконец, сам Redux Store. Нам нужны типы для всего глобального состояния (RootState) и для функции dispatch. Это критически важно для работы с хуками useSelector и useDispatch в React.
Обычно мы получаем эти типы из самого сконфигурированного хранилища.
import { combineReducers, configureStore, ThunkAction, Action,} from '@reduxjs/toolkit'; // Импортируем из Redux Toolkitimport { userReducer } from './user/reducer';import { AppState } from './types'; // Импортируем наш тип AppState
// Объединяем все редюсерыconst rootReducer = combineReducers<AppState>({ user: userReducer, // ...другие редюсеры // products: productsReducer, // isLoading: loadingReducer, // error: errorReducer,});
export const store = configureStore({ reducer: rootReducer, // middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(myLogger), // devTools: process.env.NODE_ENV !== 'production',});
// Тип для всего состояния приложения, выведенный из rootReducerexport type RootState = ReturnType<typeof store.getState>;
// Тип для функции dispatch, включая Thunksexport type AppDispatch = typeof store.dispatch;
// Тип для Thunk'ов, если вы их используетеexport type AppThunk<ReturnType = void> = ThunkAction< ReturnType, RootState, unknown, // Extra arguments for thunks Action<string>>;
// src/hooks.ts (для удобства использования в React-компонентах)import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';// Импортируем наши типизированные AppDispatch и RootStateimport type { RootState, AppDispatch } from './store';
// Используем TypedUseSelectorHook и useDispatch для создания типизированных версий хуков// Это позволит вам получить корректную типизацию состояния в useSelectorexport const useAppDispatch: () => AppDispatch = useDispatch;
// Это позволит вам получить корректную типизацию частей состояния из useSelectorexport const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;Теперь, когда вы используете useAppSelector в компоненте, вы получите полное автодополнение и проверку типов для всего вашего Redux-состояния!
import React from 'react';import { useAppSelector } from '../hooks'; // Импортируем наш типизированный хук
const UserProfileDisplay: React.FC = () => { // Теперь TypeScript знает структуру userProfile const userProfile = useAppSelector((state) => state.user); const theme = useAppSelector((state) => state.user.preferences.theme);
return ( <div> <h2>Профиль пользователя</h2> <p>Имя пользователя: {userProfile.username}</p> <p>Email: {userProfile.email}</p> <p>Аутентифицирован: {userProfile.isAuthenticated ? 'Да' : 'Нет'}</p> <p>Тема: {theme}</p> </div> );};
export default UserProfileDisplay;🔄 Redux Toolkit (RTK) — Наш Спаситель
Заголовок раздела «🔄 Redux Toolkit (RTK) — Наш Спаситель»Если вы еще не используете Redux Toolkit, самое время начать! RTK значительно упрощает работу с Redux, особенно в плане типизации, так как он максимально использует вывод типов TypeScript. Большинство бойлерплейта, который мы писали выше, RTK генерирует сам.