ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Redux 与 TypeScript 类型化实战指南:Store 推导、类型化 Hooks 与 RTK 类型安全最佳实践

2026/9/18 17:43:19 拓冰建站 浏览量
Redux 与 TypeScript 类型化实战指南:Store 推导、类型化 Hooks 与 RTK 类型安全最佳实践 Redux 与 TypeScript 类型化实战指南Store 推导、类型化 Hooks 与 RTK 类型安全最佳实践【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/reduxTypeScript 为 Redux 提供了编译期类型检查能力让 reducer、state、action creator 与 UI 组件之间的契约在开发阶段即可被验证。本篇指南以 Redux 官方文档的 TypeScript 用法为核心结合仓库中 Redux 核心类型定义src/types/与官方 TypeScript 示例项目examples/counter-ts/系统讲解从 store 类型推导、类型化 Hooks、slice 类型定义到 middleware、thunk、connect与 Redux Toolkit 各 API 的完整类型化方案。读完本篇你将掌握一套类型安全与类型声明数量之间最佳权衡的标准模式并能在团队项目中落地可维护的 Redux TypeScript 工程。为什么在 Redux 应用中使用 TypeScriptTypeScript 是 JavaScript 的类型化超集可为源码提供编译期检查。将其与 Redux 结合使用时可以带来三类直接收益类型安全覆盖 reducer、state、action creator 以及 UI 组件之间的数据流错误在编译期而非运行期暴露重构安全类型化代码在改名、调整结构时可以由编译器定位所有受影响位置重构更放心团队协作体验类型本身就是文档新成员接入代码库时对 state 形状、dispatch 能力一目了然。Redux 官方风格指南也明确强烈建议在 Redux 应用中使用静态类型。当然TypeScript 并非没有代价需要编写额外类型代码、理解泛型与工具类型等语法、并调整构建流程。我们主张务实地使用 TypeScript——在大型代码库中其收益远超额外开销但每个团队仍应权衡后自行决策。需要注意Redux 代码存在多种可行的类型检查方案本篇展示的是官方推荐的标准模式并非穷举式指南。遵循这些模式能在类型安全与需要手写的类型声明数量之间取得最佳平衡。标准 Redux Toolkit 项目中的 TypeScript 设置Redux 官方文档假设一个典型的 Redux 项目同时使用Redux ToolkitRTK与React Redux。RTK 是编写现代 Redux 逻辑的标准方式其本身以 TypeScript 编写API 设计天然对 TS 友好React Redux 的类型定义则维护在独立的types/react-redux包中从 React Redux v7.2.3 起react-redux包直接依赖该类型包类型定义会随库自动安装无需手动处理如确实需要可执行npm install types/react-redux。仓库中的 examples/counter-ts 正是按照下述模式搭建的官方 TypeScript 示例包含src/app/store.ts、src/app/hooks.ts、features/counter/counterSlice.ts与features/counter/Counter.tsx的完整结构可作为直接可运行的参考模板。定义 RootState 与 Dispatch 类型使用configureStore创建 store不需要任何额外类型声明——store 的类型完全由传入的 reducer 推断而来。你需要做的是把两个关键类型从 store 本身提取出来import { configureStore } from reduxjs/toolkit // ... export const store configureStore({ reducer: { posts: postsReducer, comments: commentsReducer, users: usersReducer } }) // Get the type of our store variable export type AppStore typeof store // Infer the RootState and AppDispatch types from the store itself export type RootState ReturnTypeAppStore[getState] // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch AppStore[dispatch]要点解析RootState ReturnTypeAppStore[getState]利用typeof store与ReturnType工具类型从 store 的getState方法返回值中推导出整个 state 树的形状AppDispatch AppStore[dispatch]直接取 store 的dispatch属性类型它包含了你在configureStore中配置的 middleware如 thunk对 dispatch 的扩展能力这两个是类型而非运行时值因此可以安全地从app/store.ts直接导出并导入到任意其他文件从 store 自身推导而非手工书写意味着当你新增 state slice 或修改 middleware 配置时RootState与AppDispatch会自动随之更新无需维护重复的类型定义。仓库中的 examples/counter-ts/src/app/store.ts 给出了同款写法并额外导出了一个AppThunk别名下文 thunk 章节详述。定义类型化 HooksTyped Hooks虽然可以在每个组件里手动引入RootState和AppDispatch但更好的做法是为useDispatch和useSelector创建预置类型的版本在应用内统一使用。原因有二对useSelector省去每次书写(state: RootState)的重复对useDispatchReact Redux 默认的Dispatch类型并不知道 thunk 的存在直接 dispatch 一个 thunk 会报类型错误。只有使用来自 store 的、包含 thunk middleware 类型的AppDispatch才能正确派发 thunk。预置类型的useAppDispatch让你不会忘记在需要的地方引入AppDispatch。由于这些是运行时变量而非类型务必把它们定义在独立的文件如app/hooks.ts中而不是 store 设置文件里——这样既能被任意组件文件导入又避免了潜在的循环导入依赖问题。使用.withTypes()预置 Hook 类型在 React Redux v9.1.0 之前预置类型的写法需要借助TypedUseSelectorHook等辅助类型各项目写法不一import type { TypedUseSelectorHook } from react-redux import { useDispatch, useSelector, useStore } from react-redux import type { AppDispatch, AppStore, RootState } from ./store // Use throughout your app instead of plain useDispatch and useSelector export const useAppDispatch: () AppDispatch useDispatch export const useAppSelector: TypedUseSelectorHookRootState useSelector export const useAppStore: () AppStore useStoreReact Redux v9.1.0 为每个 Hook 新增了.withTypes方法与 Redux Toolkit 中createAsyncThunk上的.withTypes方法同源写法简化为import { useDispatch, useSelector, useStore } from react-redux import type { AppDispatch, AppStore, RootState } from ./store // Use throughout your app instead of plain useDispatch and useSelector export const useAppDispatch useDispatch.withTypesAppDispatch() export const useAppSelector useSelector.withTypesRootState() export const useAppStore useStore.withTypesAppStore()仓库示例 examples/counter-ts/src/app/hooks.ts 正是采用.withTypes写法同时导出了useAppDispatch与useAppSelector可直接对照使用。应用代码中的类型化实践定义 Slice State 与 Action 类型每个 slice 文件都应为其初始状态值定义一个类型这样createSlice才能在各个 case reducer 中正确推断state的类型。所有生成的 action 应使用 Redux Toolkit 的PayloadActionT类型标注其泛型参数即action.payload字段的类型。在 slice 文件中可以安全地导入RootState类型——虽然这形成了循环导入store 又引用 slice reducer但 TypeScript 编译器能正确处理仅用于类型的循环引用这在编写 selector 函数时是必要且常见的import { createSlice, PayloadAction } from reduxjs/toolkit import type { RootState } from ../../app/store // Define a type for the slice state interface CounterState { value: number } // Define the initial state using that type const initialState: CounterState { value: 0 } export const counterSlice createSlice({ name: counter, // createSlice will infer the state type from the initialState argument initialState, reducers: { increment: state { state.value 1 }, decrement: state { state.value - 1 }, // Use the PayloadAction type to declare the contents of action.payload incrementByAmount: (state, action: PayloadActionnumber) { state.value action.payload } } }) export const { increment, decrement, incrementByAmount } counterSlice.actions // Other code such as selectors can use the imported RootState type export const selectCount (state: RootState) state.counter.value export default counterSlice.reducer生成的 action creator 会根据你在 reducer 中提供的PayloadActionT类型被正确标注例如incrementByAmount要求调用时必须传入一个number参数传入其他类型会在编译期报错。关于初始状态类型收窄的已知问题在某些情况下 TypeScript 会不必要地收窄初始状态的类型例如字面量类型被推断为更窄的常量导致后续赋值报错。官方给出的规避手段是改用as断言而不是声明变量类型// Workaround: cast state instead of declaring variable type const initialState { value: 0 } as CounterState仓库示例 examples/counter-ts/src/features/counter/counterSlice.ts 定义了带status: idle | loading | failed联合类型的CounterState并使用createAsyncThunk的extraReducers与之配合是 slice 类型化的完整范例。在组件中使用类型化 Hooks组件文件中应导入预置类型的 Hooks而不是直接使用 React Redux 的标准 Hooksimport React, { useState } from react import { useAppSelector, useAppDispatch } from app/hooks import { decrement, increment } from ./counterSlice export function Counter() { // The state arg is correctly typed as RootState already const count useAppSelector(state state.counter.value) const dispatch useAppDispatch() // omit rendering logic }useAppSelector的回调参数state已被自动标注为RootState返回值类型则由选择器函数推断useAppDispatch返回的dispatch可以派发包括 thunk 在内的所有 action。仓库示例 examples/counter-ts/src/features/counter/Counter.tsx 中即通过useAppSelector(selectCount)与useAppDispatch()完成数据读取与派发。用 ESLint 拦截错误的 Hook 导入团队协作中成员可能无意中导入react-redux原生的useSelector/useDispatch从而丢失类型化能力。官方推荐使用typescript-eslint的no-restricted-imports规则在导入时给出警告。示例 ESLint 配置no-restricted-imports: off, typescript-eslint/no-restricted-imports: [ warn, { name: react-redux, importNames: [useSelector, useDispatch], message: Use typed hooks useAppDispatch and useAppSelector instead. } ],注意需要先将原生no-restricted-imports关闭置为off再启用typescript-eslint/no-restricted-imports避免两条规则同时生效产生冲突。类型化额外的 Redux 逻辑类型化 ReducerReducer 是接收当前state与传入action、返回新state的纯函数。如果使用createSlice通常很少需要单独为 reducer 写类型标注只有当手写独立 reducer 时才需要声明initialState的类型并把action标注为UnknownActionimport { UnknownAction } from redux interface CounterState { value: number } const initialState: CounterState { value: 0 } export default function counterReducer( state initialState, action: UnknownAction ) { // logic here }从源码看src/types/actions.ts 中定义了三个逐层细化的 action 类型基础ActionT只要求type: T字段UnknownAction在Action基础上允许任意额外属性值为unknown专门用于Reducer类型的标注AnyAction则是已被标记为deprecated的旧类型额外属性值为any。新代码应优先使用UnknownAction而非AnyAction前者对额外属性的处理更安全。同时Redux 核心还导出了ReducerState, Action类型可用于显式标注import { Reducer } from redux const counterReducer: ReducerCounterState (state initialState, action) { // ... }源码 src/types/reducers.ts 中ReducerS, A, PreloadedState的定义为(state: S | PreloadedState | undefined, action: A) S其中A默认即UnknownAction。类型化 MiddlewareMiddleware 是 Redux store 的扩展机制它们被组合进一条管道包裹 store 的dispatch方法并能访问 store 的dispatch与getState。Redux 核心导出的Middleware类型可直接用于标注中间件函数export interface Middleware DispatchExt {}, // optional override return behavior of dispatch S any, // type of the Redux store state D extends Dispatch Dispatch // type of the dispatch method 源码 src/types/middleware.ts 中的实现与文档完全一致MiddlewareDispatchExt, S, D接收一个MiddlewareAPID, S即{ dispatch: D, getState: () S }返回一个包装next的函数。自定义中间件应使用Middleware类型并按需传入Sstate与Ddispatch两个泛型参数import { Middleware } from redux import { RootState } from ../store export const exampleMiddleware: Middleware {}, // Most middleware do not modify the dispatch return value RootState storeApi next action { const state storeApi.getState() // correctly typed as RootState }三个注意事项{}与typescript-eslint的ban-types规则若启用该规则可能会对{}报错但其推荐的修改方式会破坏 Redux store 的类型应针对该行禁用规则并继续使用{}D泛型通常只有在中间件内部还要派发额外 thunk 时才需要提供循环类型引用问题当type RootState ReturnTypetypeof store.getState时中间件与 store 定义之间可能产生循环类型引用。规避方法是把RootState改为基于根 reducer 推导const rootReducer combineReducers({ ... }); type RootState ReturnTypetypeof rootReducer;对应到 Redux Toolkit 的写法则是先在configureStore之外用combineReducers组合根 reducer再把它作为configureStore的reducer字段传入// instead of defining the reducers in the reducer field of configureStore, combine them here: const rootReducer combineReducers({ counter: counterReducer }) // then set rootReducer as the reducer object of configureStore const store configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware getDefaultMiddleware().concat(yourMiddleware) }) type RootState ReturnTypetypeof rootReducer类型化 Redux ThunkRedux Thunk 是编写与 store 交互的同步/异步逻辑的标准中间件。thunk 函数接收dispatch与getState作为参数Redux Thunk 内置的ThunkAction类型可用来标注这些参数export type ThunkAction R, // Return type of the thunk function S, // state type used by getState E, // any extra argument injected into the thunk A extends Action // known types of actions that can be dispatched (dispatch: ThunkDispatchS, E, A, getState: () S, extraArgument: E) R通常你需要提供Rthunk 的返回类型与Sstate 类型。由于 TypeScript不允许只提供部分泛型参数其余参数的惯用值是E用unknownA用UnknownActionimport { UnknownAction } from redux import { sendMessage } from ./store/chat/actions import { RootState } from ./store import { ThunkAction } from redux-thunk export const thunkSendMessage (message: string): ThunkActionvoid, RootState, unknown, UnknownAction async dispatch { const asyncResp await exampleAPI() dispatch( sendMessage({ message, user: asyncResp, timestamp: new Date().getTime() }) ) } function exampleAPI() { return Promise.resolve(Async Chat Bot) }为减少重复书写建议在 store 文件中一次性定义可复用的AppThunk类型之后所有 thunk 都复用它export type AppThunkReturnType void ThunkAction ReturnType, RootState, unknown, UnknownAction 该类型假设 thunk 没有有意义的返回值若 thunk 返回 Promise 且需要在组件中检查 thunk 的派发结果则使用AppThunkPromiseSomeReturnType。仓库示例 examples/counter-ts/src/app/store.ts 中的AppThunk使用Actionstring作为第四个泛型参数旧写法并在 counterSlice.ts 中演示了手写 thunkincrementIfOdd它以AppThunk为返回类型在函数体内通过getState()读取当前值并条件派发 action。:::caution 不要忘记默认的useDispatch不知道 thunk 的存在直接用它 dispatch thunk 会产生类型错误。务必使用包含 thunk 中间件类型的AppDispatch版本见上文定义 RootState 与 Dispatch 类型。 :::与 React Redux 的配合使用React Redux 虽是独立于 Redux 的库但通常与 React 一起使用。其类型定义维护在 DefinitelyTyped 中但作为react-redux的依赖会自动安装如需手动安装可执行npm install types/react-redux类型化useSelector在 selector 函数中声明state参数的类型useSelector的返回值类型即可自动推断为 selector 的返回类型interface RootState { isOn: boolean } // TS infers type: (state: RootState) boolean const selectIsOn (state: RootState) state.isOn // TS infers isOn is boolean const isOn useSelector(selectIsOn)也可以内联书写const isOn useSelector((state: RootState) state.isOn)但更推荐的方式是优先创建预置了正确state类型的useAppSelectorHook见上文 Typed Hooks 一节。类型化useDispatch默认情况下useDispatch()的返回值就是 Redux 核心类型定义的标准Dispatch类型无需额外声明const dispatch useDispatch()从源码 src/types/store.ts 可以看到DispatchA extends Action UnknownAction的定义它是一个可调用签名T extends A(action: T, ...extraArgs: any[]) T默认只接受UnknownAction。因此若需要派发 thunk同样推荐优先创建预置了正确Dispatch类型的useAppDispatchHook。类型化connect高阶组件如果仍在使用connect应使用types/react-reduxv7.1.2导出的ConnectedPropsT类型从connect的结果自动推断props 类型。关键在于把connect(mapState, mapDispatch)(MyComponent)拆成两步import { connect, ConnectedProps } from react-redux interface RootState { isOn: boolean } const mapState (state: RootState) ({ isOn: state.isOn }) const mapDispatch { toggleOn: () ({ type: TOGGLE_IS_ON }) } const connector connect(mapState, mapDispatch) // The inferred type will look like: // {isOn: boolean, toggleOn: () void} type PropsFromRedux ConnectedPropstypeof connector type Props PropsFromRedux { backgroundColor: string } const MyComponent (props: Props) ( div style{{ backgroundColor: props.backgroundColor }} button onClick{props.toggleOn} Toggle is {props.isOn ? ON : OFF} /button /div ) export default connector(MyComponent)Props PropsFromRedux { backgroundColor: string }通过交叉类型把 Redux 注入的 props 与组件自身的 props 合并connect的mapState/mapDispatch类型也随之得到校验。与 Redux Toolkit 的配合使用上文标准 Redux Toolkit 项目设置已覆盖configureStore与createSlice的常规用法此处补充 RTK 场景下常见的一些进阶类型化模式。类型化configureStoreconfigureStore会从传入的根 reducer 函数自动推断 state 的类型通常不需要任何类型声明。若要添加额外中间件务必使用getDefaultMiddleware()返回数组的.concat()与.prepend()方法——它们能正确保留所添加中间件的类型直接使用普通数组展开写法往往会丢失这些类型信息const store configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware getDefaultMiddleware() .prepend( // correctly typed middlewares can just be used additionalMiddleware, // you can also type middlewares manually untypedMiddleware as Middleware (action: ActionspecialAction) number, RootState ) // prepend and concat calls can be chained .concat(logger) })匹配 ActionMatching ActionsRTK 生成的 action creator 自带match方法它充当 类型谓词type predicate调用someActionCreator.match(action)会对action.type做字符串比较若用作条件判断会把action的类型收窄到正确的 TS 类型const increment createActionnumber(increment) function test(action: Action) { if (increment.match(action)) { // action.payload inferred correctly here const num 5 action.payload } }这在 Redux 中间件、redux-observable或 RxJS 的filter方法中检查 action 类型时尤其有用。类型化createSlice定义独立的 Case Reducer当内联定义的 case reducer 过多显得混乱或需要在多个 slice 间复用 case reducer 时可以在createSlice外部定义并使用CaseReducer类型标注type State number const increment: CaseReducerState, PayloadActionnumber (state, action) state action.payload createSlice({ name: test, initialState: 0, reducers: { increment } })类型化extraReducers为createSlice添加extraReducers字段时务必使用builder callback 形式——plain object 形式无法正确推断 action 类型。向builder.addCase()传入 RTK 生成的 action creatoraction的类型即可被正确推断const usersSlice createSlice({ name: users, initialState, reducers: { // fill in primary logic here }, extraReducers: builder { builder.addCase(fetchUserById.pending, (state, action) { // both state and action are now correctly typed // based on the slice state and the pending action creator }) } })类型化prepare回调若需要为 action 添加meta或error属性或自定义payload必须使用prepare记法定义 case reducerconst blogSlice createSlice({ name: blogData, initialState, reducers: { receivedAll: { reducer( state, action: PayloadActionPage[], string, { currentPage: number } ) { state.all action.payload state.meta action.meta }, prepare(payload: Page[], currentPage: number) { return { payload, meta: { currentPage } } } } } })这里PayloadActionPage[], string, { currentPage: number }的第三个泛型参数即meta字段的类型与prepare返回的{ payload, meta }结构严格对应。修复导出 Slice 时的循环类型在极少数情况下可能需要用特定类型导出 slice reducer 以打破循环类型依赖export default counterSlice.reducer as ReducerCounter类型化createAsyncThunk基础用法下只需为createAsyncThunk提供载荷创建回调payload creator的单参数类型并确保回调返回值类型正确const fetchUserById createAsyncThunk( users/fetchById, // Declare the type your function argument here: async (userId: number) { const response await fetch(https://reqres.in/api/users/${userId}) // Inferred return type: PromiseMyData return (await response.json()) as MyData } ) // the parameter of fetchUserById is automatically inferred to number here // and dispatching the resulting thunkAction will return a Promise of a correctly // typed fulfilled or rejected action. const lastReturnedAction await store.dispatch(fetchUserById(3))此时fetchUserById的参数自动推断为number派发得到的 thunk action 会返回一个类型正确的Promiseresolve 为 fulfilled 或 rejected action。若需要修改thunkApi参数的类型例如指定getState()返回的 state 类型则必须提供前两个泛型参数返回值类型与载荷参数类型再把需要用到的 thunkApi 字段类型 放在第三个泛型对象中const fetchUserById createAsyncThunk // Return type of the payload creator MyData, // First argument to the payload creator number, { // Optional fields for defining thunkApi field types dispatch: AppDispatch state: State extra: { jwt: string } } (users/fetchById, async (userId, thunkApi) { const response await fetch(https://reqres.in/api/users/${userId}, { headers: { Authorization: Bearer ${thunkApi.extra.jwt} } }) return (await response.json()) as MyData })类型化createEntityAdaptercreateEntityAdapter的 TypeScript 用法取决于实体是按id属性规范化还是需要自定义selectId。情况一实体自带id属性。只需把实体类型作为唯一泛型参数传入无需selectIdinterface Book { id: number title: string } // no selectId needed here, as the entity has an id property we can default to const booksAdapter createEntityAdapterBook({ sortComparer: (a, b) a.title.localeCompare(b.title) }) const booksSlice createSlice({ name: books, // The type of the state is inferred here initialState: booksAdapter.getInitialState(), reducers: { bookAdded: booksAdapter.addOne, booksReceived(state, action: PayloadAction{ books: Book[] }) { booksAdapter.setAll(state, action.payload.books) } } })情况二实体按其他属性规范化。推荐传入自定义selectId函数并在函数参数处标注实体类型这样 ID 的类型能被正确推断无需手动提供interface Book { bookId: number title: string // ... } const booksAdapter createEntityAdapter({ selectId: (book: Book) book.bookId, sortComparer: (a, b) a.title.localeCompare(b.title) }) const booksSlice createSlice({ name: books, // The type of the state is inferred here initialState: booksAdapter.getInitialState(), reducers: { bookAdded: booksAdapter.addOne, booksReceived(state, action: PayloadAction{ books: Book[] }) { booksAdapter.setAll(state, action.payload.books) } } })其他推荐实践默认使用 React Redux Hooks API官方推荐以 Hooks API 作为默认方案useSelector接收 selector 函数返回值类型从state参数的类型轻松推断与 TypeScript 配合极其顺畅。虽然connect仍可用且可以被类型化但正确类型化connect要困难得多。避免创建 Action 类型联合Union官方明确不建议创建 action 类型的联合union——它既无实际收益还会在某些方面误导编译器例如 TS 会优化掉 union 中看似未使用的部分、破坏switch语句中的穷尽性检查等。此外如果使用了createSlice你已经可以确认该 slice 定义的所有 action 都被正确处理联合类型更无必要。在中间件或 reducer 中检查 action 时应优先使用 action creator 自带的match类型谓词方法。资源索引以下为本篇涉及到的仓库内关键资源便于进一步深入阅读Redux 核心类型定义src/types/actions.tsAction、UnknownAction、AnyAction、ActionCreator的定义src/types/reducers.tsReducer及StateFromReducersMapObject等状态推导工具类型src/types/middleware.tsMiddleware与MiddlewareAPI的定义src/types/store.tsStore、Dispatch、StoreCreator、StoreEnhancer的定义官方 TypeScript 示例项目examples/counter-ts/src/app/store.tsRootState/AppDispatch/AppThunk推导examples/counter-ts/src/app/hooks.ts.withTypes预置类型 Hooksexamples/counter-ts/src/features/counter/counterSlice.tsslice 类型化、createAsyncThunk与手写 thunkexamples/counter-ts/src/features/counter/Counter.tsx组件中使用类型化 Hooks相关官方文档docs/style-guide/style-guide.mdRedux 风格指南含使用静态类型的建议docs/tutorials/fundamentals/part-3-state-actions-reducers.mdreducer 基础docs/tutorials/fundamentals/part-4-store.mdstore 与 middleware 概念docs/tutorials/essentials/part-5-async-logic.md异步逻辑与 thunk 结果检查类型级测试仓库 test/typescript/ 目录下存放了一组*.test-d.ts类型测试文件覆盖store、dispatch、middleware、reducers、compose等主题以类型断言方式验证 Redux 各类型定义的行为可作为理解类型边界的参考。【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考