使用 TypeScript
- 使用 TypeScript 设置 Redux 应用程序的标准模式
- 正确为 Redux 逻辑部分进行类型化的技巧
- 了解 TypeScript 语法和术语
- 熟悉 TypeScript 概念,例如 泛型 和 实用类型
- 了解 React Hooks
概述
TypeScript 是 JavaScript 的类型化超集,它提供源代码的编译时检查。当与 Redux 一起使用时,TypeScript 可以帮助提供
- 对 reducer、状态和 action creator 以及 UI 组件的类型安全
- 轻松重构类型化代码
- 在团队环境中提供更优越的开发体验
我们强烈建议在 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 在一个单独的 @types/react-redux
类型定义包 中包含其类型定义。除了对库函数进行类型化之外,这些类型还导出了一些帮助程序,使编写 Redux 存储和 React 组件之间类型安全的接口变得更加容易。
从 React Redux v7.2.3 开始,react-redux
包依赖于 @types/react-redux
,因此类型定义将与库一起自动安装。否则,您需要手动安装它们(通常是 npm install @types/react-redux
)。
用于 Create-React-App 的 Redux+TS 模板 附带了已配置的这些模式的工作示例。
定义根状态和调度类型
使用 configureStore 不需要任何额外的类型化。但是,您需要提取 RootState
类型和 Dispatch
类型,以便可以根据需要引用它们。从存储本身推断这些类型意味着它们会在您添加更多状态切片或修改中间件设置时正确更新。
由于这些是类型,因此可以安全地将它们直接从存储设置文件(例如 app/store.ts
)导出,并直接导入到其他文件中。
import { configureStore } from '@reduxjs/toolkit'
// ...
export const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer
}
})
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
定义类型化钩子
虽然可以将 RootState
和 AppDispatch
类型导入到每个组件中,但最好为应用程序中的使用创建 useDispatch
和 useSelector
钩子的预类型化版本。这对于几个原因很重要
- 对于
useSelector
,它可以节省您每次都键入(state: RootState)
的需要 - 对于
useDispatch
,默认的Dispatch
类型不知道 thunk 或其他中间件。为了正确调度 thunk,您需要使用来自存储的特定自定义AppDispatch
类型(包括 thunk 中间件类型),并将其与useDispatch
一起使用。添加预类型化的useDispatch
钩子可以防止您忘记在需要的地方导入AppDispatch
。
由于这些是实际的变量,而不是类型,因此在单独的文件(例如 app/hooks.ts
)中定义它们很重要,而不是存储设置文件。这使您可以将它们导入到需要使用这些钩子的任何组件文件中,并避免潜在的循环导入依赖关系问题。
.withTypes()
以前,使用应用程序设置“预类型化”钩子的方法略有不同。结果看起来像下面的代码片段
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
方法。
现在设置变为
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
的类型。
所有生成的 action 应该使用 Redux Toolkit 中的 PayloadAction<T>
类型定义,该类型将 action.payload
字段的类型作为其泛型参数。
您可以安全地从 store 文件中导入 RootState
类型。这是一个循环导入,但 TypeScript 编译器可以正确处理类型。这可能需要用于编写选择器函数等用例。
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
生成的 action 创建者将被正确类型化为接受一个 payload
参数,该参数基于您为 reducer 提供的 PayloadAction<T>
类型。例如,incrementByAmount
需要一个 number
作为其参数。
在某些情况下,TypeScript 可能会不必要地收紧初始状态的类型。如果发生这种情况,您可以通过使用 as
转换初始状态来解决它,而不是声明变量的类型
// Workaround: cast state instead of declaring variable type
const initialState = {
value: 0
} as CounterState
在组件中使用类型化钩子
在组件文件中,导入预类型化的钩子,而不是从 React Redux 中导入标准钩子。
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 存储的扩展机制。中间件被组合成一个管道,它包装了存储的 dispatch
方法,并可以访问存储的 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
>
自定义中间件应该使用 `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 存储类型出现问题,您应该为该行禁用该规则并继续使用 `{}`。
调度泛型可能只在您在中间件中调度额外的 thunk 时才需要。
在使用 `type RootState = ReturnType<typeof store.getState>` 的情况下,可以通过将 `RootState` 的类型定义更改为以下方式来避免中间件和存储定义之间的 循环类型引用
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 存储交互的同步和异步逻辑的标准中间件。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: ThunkDispatch<S, E, A>, getState: () => S, extraArgument: E) => R
您通常需要提供 `R`(返回值类型)和 `S`(状态)泛型参数。不幸的是,TS 不允许只提供一些泛型参数,因此其他参数的常用值为 `unknown`(对于 `E`)和 `UnknownAction`(对于 `A`)。
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')
}
为了减少重复,您可能希望在您的存储文件中定义一个可重用的 `AppThunk` 类型,然后在编写 thunk 时使用该类型。
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
UnknownAction
>
请注意,这假设 thunk 没有有意义的返回值。如果您的 thunk 返回一个 promise,并且您希望 在调度 thunk 后使用返回的 promise,您需要将其用作 `AppThunk<Promise<SomeReturnType>>`。
不要忘记**默认的useDispatch
钩子不知道thunk**,因此分派thunk会导致类型错误。请务必在您的组件中使用更新的Dispatch
形式,该形式将thunk识别为可接受的派发类型。
与 React Redux 一起使用
虽然React Redux是一个独立于 Redux 本身的库,但它通常与 React 一起使用。
有关如何在 TypeScript 中正确使用 React Redux 的完整指南,请参阅React Redux 文档中的“静态类型”页面。本节将重点介绍标准模式。
如果您使用的是 TypeScript,React Redux 类型在 DefinitelyTyped 中单独维护,但作为 react-redux 包的依赖项包含在内,因此它们应该自动安装。如果您仍然需要手动安装它们,请运行
npm install @types/react-redux
为 useSelector
钩子添加类型
在选择器函数中声明state
参数的类型,useSelector
的返回类型将被推断为与选择器的返回类型匹配
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)
但是,最好使用预先添加类型的useAppSelector
钩子,其中内置了正确的state
类型。
为 useDispatch
钩子添加类型
默认情况下,useDispatch
的返回值是 Redux 核心类型定义的标准Dispatch
类型,因此不需要声明
const dispatch = useDispatch()
但是,最好使用预先添加类型的useAppDispatch
钩子,其中内置了正确的Dispatch
类型。
为 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 一起使用
使用 TypeScript 的标准 Redux Toolkit 项目设置部分已经涵盖了configureStore
和createSlice
的正常使用模式,以及Redux Toolkit 的“使用 TypeScript”页面详细介绍了所有 RTK API。
以下是使用 RTK 时您会经常看到的一些额外输入模式。
输入 configureStore
configureStore
从提供的根 reducer 函数推断状态值的类型,因此不需要任何特定的类型声明。
如果您想向商店添加额外的中间件,请务必使用 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
定义单独的案例 reducer
如果您有太多案例 reducer,并且内联定义它们会很混乱,或者您想在切片之间重用案例 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
回调
如果您想向操作添加 meta
或 error
属性,或自定义操作的 payload
,则必须使用 prepare
符号来定义案例 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
提供其负载创建回调的单个参数的类型。您还应确保回调的返回值类型正确。
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
的类型,则必须提供返回类型和负载参数的前两个泛型参数,以及对象中相关的“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
添加类型只需要您指定实体类型作为唯一的泛型参数。这通常看起来像
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
是一个简单的 hook,它接受一个选择器函数,返回值类型可以从state
参数的类型轻松推断出来。
虽然connect
仍然可以正常工作,并且可以添加类型,但正确添加类型要困难得多。
避免操作类型联合
我们特别建议不要尝试创建操作类型的联合,因为它没有实际的好处,实际上在某些方面会误导编译器。有关这为什么是一个问题的解释,请参阅 RTK 维护者 Lenz Weber 的文章 不要使用 Redux 操作类型创建联合类型。
此外,如果您使用的是createSlice
,您已经知道该切片定义的所有操作都已正确处理。
资源
有关更多信息,请参阅以下其他资源
- Redux 库文档
- React Redux 文档:静态类型:使用 TypeScript 与 React Redux API 的示例
- Redux Toolkit 文档:使用 TypeScript:使用 TypeScript 与 Redux Toolkit API 的示例
- React + Redux + TypeScript 指南
- React+TypeScript 速查表:使用 TypeScript 与 React 的综合指南
- React + Redux in TypeScript 指南: 关于使用 React 和 Redux 与 TypeScript 的模式的广泛信息
- 注意:虽然本指南包含一些有用的信息,但其中展示的许多模式与本页中显示的推荐实践相冲突,例如使用动作类型联合。我们出于完整性考虑链接了此指南
- 其他文章