跳至主内容
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

TypeScript 使用指南

学习内容
  • 使用 TypeScript 配置 Redux 应用的标准模式
  • 为 Redux 逻辑部分正确添加类型标注的技巧
前置知识

概述

TypeScript 是 JavaScript 的类型化超集,可在编译时对源代码进行检查。与 Redux 结合使用时,TypeScript 能够提供:

  1. 为 reducers、状态、动作创建器以及 UI 组件提供类型安全

  2. 轻松重构已添加类型标注的代码

  3. 在团队协作中提供更卓越的开发体验

我们强烈建议在 Redux 应用中使用 TypeScript。然而,与所有工具一样,TypeScript 也存在权衡取舍。它增加了编写额外代码、理解 TS 语法以及构建应用的复杂性。同时,它通过更早捕获开发错误、实现更安全高效的代码重构以及充当现有源代码的文档来提供价值。

我们相信,实用主义的 TypeScript 使用方式所提供的价值和收益足以抵消其额外开销,尤其在大型代码库中。但您仍需花时间评估权衡因素,以决定是否在您的应用中使用 TS

存在多种对 Redux 代码进行类型检查的方法。本文展示我们推荐的标准模式,用于结合使用 Redux 和 TypeScript,但并非详尽指南。遵循这些模式将带来良好的 TS 使用体验,实现类型安全性与代码库中必须添加的类型声明数量之间的最佳平衡

使用 TypeScript 的标准 Redux Toolkit 项目配置

我们假设典型的 Redux 项目会同时使用 Redux Toolkit 和 React Redux。

Redux Toolkit (RTK) 是编写现代 Redux 逻辑的标准方案。RTK 本身使用 TypeScript 编写,其 API 设计旨在提供良好的 TypeScript 使用体验。

React Redux 的类型定义位于 NPM 上独立的 @types/react-redux 类型定义包中。除了为库函数提供类型标注外,这些类型还导出一些辅助工具,以便在 Redux store 和 React 组件之间编写类型安全的接口。

自 React Redux v7.2.3 起,react-redux 包已依赖 @types/react-redux,因此类型定义会随库自动安装。否则,您需要手动安装(通常使用 npm install @types/react-redux)。

Create-React-App 的 Redux+TS 模板已包含这些模式的配置示例。

定义根状态与派发类型

使用 configureStore 无需任何额外的类型标注。但您需要提取 RootState 类型和 Dispatch 类型以便在需要时引用。从 store 本身推断这些类型意味着它们会随着您添加更多状态切片或修改中间件设置而正确更新。

由于这些是类型声明,可直接从 app/store.ts 等 store 配置文件导出,并直接导入到其他文件中使用。

app/store.ts
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 = ReturnType<AppStore['getState']>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch']

定义类型化钩子

虽然可以将 RootStateAppDispatch 类型导入每个组件,但更推荐创建预定义类型的 useDispatchuseSelector 钩子版本供应用中使用。这样做很重要,原因如下:

  • 对于 useSelector,可避免每次手动指定 (state: RootState)

  • 对于 useDispatch 钩子:默认的 Dispatch 类型无法识别 thunk 或其他中间件。要正确派发 thunk,需使用 store 中已包含 thunk 中间件类型的定制化 AppDispatch 类型,并在 useDispatch 中应用。预定义类型化的 useDispatch 钩子可避免在需要时忘记导入 AppDispatch

由于这些是实际变量而非类型声明,应在独立文件(如 app/hooks.ts)中定义,而非 store 配置文件。这样可避免循环依赖问题,并支持在任意组件中导入使用。

.withTypes() 方法

此前,为应用设置预定义钩子的方式较为多样。典型实现如下代码片段所示:

app/hooks.ts
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: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore: () => AppStore = useStore

React Redux v9.1.0 为这些钩子新增了 .withTypes 方法,类似于 Redux Toolkit 中 createAsyncThunk.withTypes 方法。

现在配置方式变为:

app/hooks.ts
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.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()

应用实践

定义切片状态与动作类型

每个切片文件应明确定义其初始状态类型,以便 createSlice 能正确推断每个 case reducer 中的 state 类型。

所有生成的动作应使用 Redux Toolkit 的 PayloadAction<T> 类型定义,其泛型参数对应 action.payload 字段的类型。

可安全地从 store 文件导入 RootState 类型。虽然属于循环导入,但 TypeScript 编译器能正确处理类型引用。这在编写选择器函数等场景中可能需要。

features/counter/counterSlice.ts
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: PayloadAction<number>) => {
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

生成的动作创建器将基于 reducer 中定义的 PayloadAction<T> 类型正确接收 payload 参数。例如 incrementByAmount 要求传入 number 类型参数。

在某些情况下,TypeScript 可能会过度收紧初始状态的类型。如果遇到这种情况,您可以通过使用 as 进行类型转换来解决,而不是声明变量类型:

// Workaround: cast state instead of declaring variable type
const initialState = {
value: 0
} as CounterState

在组件中使用类型化钩子

在组件文件中,请从 React Redux 导入预定义类型化的钩子,而非标准钩子。

features/counter/Counter.tsx
import 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
}
错误导入警告

ESLint 可帮助团队轻松导入正确的钩子。当意外使用错误导入时,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."
}
],

类型化扩展 Redux 逻辑

类型校验 Reducer

Reducer 是纯函数,接收当前 state 和传入的 action 作为参数,并返回新状态。

若使用 Redux Toolkit 的 createSlice,通常无需单独为 reducer 添加类型声明。如需编写独立 reducer,声明 initialState 值的类型并将 action 类型设为 UnknownAction 通常已足够:

import { UnknownAction } from 'redux'

interface CounterState {
value: number
}

const initialState: CounterState = {
value: 0
}

export default function counterReducer(
state = initialState,
action: UnknownAction
) {
// logic here
}

Redux 核心库也导出了 Reducer<State, Action> 类型可供使用。

类型校验中间件

中间件是 Redux store 的扩展机制。多个中间件组合成管道包装 store 的 dispatch 方法,并有权访问 store 的 dispatchgetState 方法。

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
>

自定义中间件应使用 Middleware 类型,必要时传递泛型参数 S (状态) 和 D (派发):

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,当在派发值中使用 {} 时,@typescript-eslint/ban-types 规则可能报错。其推荐修改方案不适用且会破坏 Redux store 类型,您应禁用该规则并保留 {} 的使用。

仅当在中间件内派发额外 thunk 时才需指定派发泛型。

若使用 type RootState = ReturnType<typeof store.getState>,通过以下方式定义 RootState 可避免中间件与 store 定义间的循环类型引用

const rootReducer = combineReducers({ ... });
type RootState = ReturnType<typeof rootReducer>;

Redux Toolkit 示例中切换 RootState 类型定义的方式:

// 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 = ReturnType<typeof rootReducer>

类型校验 Redux Thunk

Redux Thunk 是用于编写与 Redux store 交互的同步/异步逻辑的标准中间件。thunk 函数接收 dispatchgetState 作为参数。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: ThunkDispatch<S, E, A>, getState: () => S, extraArgument: E) => R

通常需要提供 R(返回类型)和 S(状态)泛型参数。由于 TS 不允许仅提供部分泛型参数,其他参数通常使用默认值:E 设为 unknownA 设为 UnknownAction

import { UnknownAction } from 'redux'
import { sendMessage } from './store/chat/actions'
import { RootState } from './store'
import { ThunkAction } from 'redux-thunk'

export const thunkSendMessage =
(message: string): ThunkAction<void, 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 类型:

export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
UnknownAction
>

注意:这假设 thunk 没有有意义的返回值。若 thunk 返回 promise 且需在派发 thunk 后使用该 promise,应声明为 AppThunk<Promise<SomeReturnType>>

注意

请注意默认的 useDispatch 钩子无法识别 thunk,直接派发 thunk 会导致类型错误。务必在组件中使用能识别 thunk 的 Dispatch 类型

与 React Redux 配合使用

虽然 React Redux 是独立于 Redux 的库,但常与 React 配合使用。

关于如何正确结合 TypeScript 使用 React Redux 的完整指南,请参阅 React Redux 文档中的"静态类型"章节。本节将重点介绍标准模式。

若使用 TypeScript,React Redux 的类型声明由 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 类型的预定义 useAppSelector 钩子。

类型化 useDispatch 钩子

默认情况下,useDispatch 的返回值是 Redux 核心类型定义的 Dispatch 类型,无需额外声明:

const dispatch = useDispatch()

但更推荐使用内置正确 Dispatch 类型的预定义 useAppDispatch 钩子。

类型化 connect 高阶组件

若仍在使用 connect,应通过 @types/react-redux^7.1.2 导出的 ConnectedProps<T> 类型自动推断 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 = ConnectedProps<typeof 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)

与 Redux Toolkit 配合使用

标准 Redux Toolkit TypeScript 项目配置 已涵盖 configureStorecreateSlice 的常规用法,Redux Toolkit 的"TypeScript 使用指南" 详细介绍了所有 RTK API。

以下是使用 RTK 时常见的附加类型模式。

类型化 configureStore

configureStore 从根 reducer 函数自动推断状态类型,通常无需显式声明类型。

如需向 store 添加额外中间件,请务必使用 getDefaultMiddleware() 返回数组中提供的专用 .concat().prepend() 方法,这些方法能正确保留所添加中间件的类型信息(使用普通 JS 数组展开运算符常会导致类型丢失)。

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: Action<'specialAction'>) => number,
RootState
>
)
// prepend and concat calls can be chained
.concat(logger)
})

动作匹配

RTK 生成的动作创建器具有 match 方法,该方法作为类型谓词使用。调用 someActionCreator.match(action) 会对 action.type 进行字符串比较,若作为条件使用,可将 action 类型收窄至正确的 TS 类型:

const increment = createAction<number>('increment')
function test(action: Action) {
if (increment.match(action)) {
// action.payload inferred correctly here
const num = 5 + action.payload
}
}

这在 Redux 中间件(如自定义中间件、redux-observable 和 RxJS 的 filter 方法)中检查动作类型时尤其有用。

createSlice 类型标注

定义独立 Case Reducer

若 case reducer 过多导致内联定义混乱,或需在切片间复用 case reducer,可在 createSlice 外部将其定义为 CaseReducer 类型:

type State = number
const increment: CaseReducer<State, PayloadAction<number>> = (state, action) =>
state + action.payload

createSlice({
name: 'test',
initialState: 0,
reducers: {
increment
}
})

extraReducers 类型标注

若在 createSlice 中添加 extraReducers 字段,请务必使用"构建器回调"形式——"纯对象"形式无法正确推断动作类型。将 RTK 生成的动作创建器传给 builder.addCase() 可正确推断 action 类型:

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 回调类型标注

如需为动作添加 metaerror 属性,或自定义动作的 payload,必须使用 prepare 符号定义 case reducer。结合 TypeScript 的用法示例如下:

const blogSlice = createSlice({
name: 'blogData',
initialState,
reducers: {
receivedAll: {
reducer(
state,
action: PayloadAction<Page[], string, { currentPage: number }>
) {
state.all = action.payload
state.meta = action.meta
},
prepare(payload: Page[], currentPage: number) {
return { payload, meta: { currentPage } }
}
}
}
})

修复导出切片的循环类型依赖

极少数情况下,可能需要为导出的切片 reducer 指定显式类型以解决循环类型依赖问题,示例如下:

export default counterSlice.reducer as Reducer<Counter>

createAsyncThunk 类型标注

基础用法中,只需为 createAsyncThunk 的 payload 创建回调参数提供类型。同时应确保回调返回值类型正确:

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: Promise<MyData>
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))

如需修改 thunkApi 参数类型(如指定 getState() 返回的 state 类型),必须提供前两个泛型参数(返回类型和 payload 参数),并在对象中声明相关的"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
})

createEntityAdapter 类型标注

createEntityAdapter 的 TypeScript 用法因实体是否通过 id 属性标准化而异,或需自定义 selectId

若实体通过 id 属性标准化,createEntityAdapter 仅需将实体类型作为单一泛型参数。例如:

interface Book {
id: number
title: string
}

// no selectId needed here, as the entity has an `id` property we can default to
const booksAdapter = createEntityAdapter<Book>({
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

建议将 React Redux Hooks API 作为默认方案。Hooks API 与 TypeScript 协同更简单:useSelector 作为基础钩子接收选择器函数,其返回类型可从 state 参数类型轻松推断。

虽然 connect 仍可使用且可进行类型标注,但其正确标注的难度显著更高。

避免动作类型联合

我们特别建议 不要 尝试创建动作类型的联合类型,这不会带来实际收益,反而在某些方面会误导编译器。关于此问题的详细解释,请参阅 RTK 维护者 Lenz Weber 的文章《请勿创建 Redux 动作类型的联合类型》

此外,如果您正在使用 createSlice,则已明确该切片定义的所有动作都能得到正确处理。

扩展资源

更多信息请参考以下资源: