Skip to main content

与 TypeScript 一起使用

¥Usage with TypeScript

你将学到什么
  • 使用 TypeScript 设置 Redux 应用的标准模式

    ¥Standard patterns for setting up a Redux app with TypeScript

  • 正确输入 Redux 逻辑部分的技术

    ¥Techniques for correctly typing portions of Redux logic

先决条件

概述

¥Overview

TypeScript 是 JavaScript 的类型超集,提供源代码的编译时检查。与 Redux 一起使用时,TypeScript 可以帮助提供:

¥TypeScript is a typed superset of JavaScript that provides compile-time checking of source code. When used with Redux, TypeScript can help provide:

  1. reducer、状态和操作创建者以及 UI 组件的类型安全

    ¥Type safety for reducers, state and action creators, and UI components

  2. 轻松重构键入的代码

    ¥Easy refactoring of typed code

  3. 团队环境中卓越的开发者体验

    ¥A superior developer experience in a team environment

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

¥We strongly recommend using TypeScript in Redux applications. However, like all tools, TypeScript has tradeoffs. It adds complexity in terms of writing additional code, understanding TS syntax, and building the application. At the same time, it provides value by catching errors earlier in development, enabling safer and more efficient refactoring, and acting as documentation for existing source code.

我们相信 TypeScript 的实用使用 提供了足够多的价值和好处来证明增加的开销是合理的,特别是在较大的代码库中,但你应该花时间评估权衡并决定是否值得在你自己的应用中使用 TS。

¥We believe that pragmatic use of TypeScript provides more than enough value and benefit to justify the added overhead, especially in larger codebases, but you should take time to evaluate the tradeoffs and decide whether it's worth using TS in your own application.

有多种可能的方法来对 Redux 代码进行类型检查。本页面显示了我们一起使用 Redux 和 TypeScript 的标准推荐模式,但并不是详尽的指南。遵循这些模式应该会带来良好的 TS 使用体验,并在类型安全性和必须添加到代码库中的类型声明数量之间进行最佳权衡。

¥There are multiple possible approaches to type checking Redux code. This page shows our standard recommended patterns for using Redux and TypeScript together, and is not an exhaustive guide. Following these patterns should result in a good TS usage experience, with the best tradeoffs between type safety and amount of type declarations you have to add to your codebase.

使用 TypeScript 设置标准 Redux 工具包项目

¥Standard Redux Toolkit Project Setup with TypeScript

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

¥We assume that a typical Redux project is using Redux Toolkit and React Redux together.

Redux 工具包 (RTK) 是编写现代 Redux 逻辑的标准方法。RTK 已经用 TypeScript 编写,其 API 旨在为 TypeScript 使用提供良好的体验。

¥Redux Toolkit (RTK) is the standard approach for writing modern Redux logic. RTK is already written in TypeScript, and its API is designed to provide a good experience for TypeScript usage.

反应还原 在 NPM 上的单独 @types/react-redux typedefs 包 中具有其类型定义。除了输入库函数之外,这些类型还导出一些辅助程序,以便更轻松地在 Redux 存储和 React 组件之间编写类型安全的接口。

¥React Redux has its type definitions in a separate @types/react-redux typedefs package on NPM. In addition to typing the library functions, the types also export some helpers to make it easier to write typesafe interfaces between your Redux store and your React components.

从 React Redux v7.2.3 开始,react-redux 包依赖于 @types/react-redux,因此类型定义将自动随库一起安装。否则,你需要自己手动安装它们(通常是 npm install @types/react-redux )。

¥As of React Redux v7.2.3, the react-redux package has a dependency on @types/react-redux, so the type definitions will be automatically installed with the library. Otherwise, you'll need to manually install them yourself (typically npm install @types/react-redux ).

Create-React-App 的 Redux+TS 模板 附带了已配置的这些模式的工作示例。

¥The Redux+TS template for Create-React-App comes with a working example of these patterns already configured.

定义根状态和调度类型

¥Define Root State and Dispatch Types

使用 configureStore 不需要任何额外的输入。但是,你将需要提取 RootState 类型和 Dispatch 类型,以便可以根据需要引用它们。从存储本身推断这些类型意味着当你添加更多状态片或修改中间件设置时它们会正确更新。

¥Using configureStore should not need any additional typings. You will, however, want to extract the RootState type and the Dispatch type so that they can be referenced as needed. Inferring these types from the store itself means that they correctly update as you add more state slices or modify middleware settings.

由于这些是类型,因此可以安全地直接从存储设置文件(例如 app/store.ts)导出它们并将它们直接导入到其他文件中。

¥Since those are types, it's safe to export them directly from your store setup file such as app/store.ts and import them directly into other files.

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']

定义类型化钩子

¥Define Typed Hooks

虽然可以将 RootStateAppDispatch 类型导入到每个组件中,但最好创建 useDispatchuseSelector 钩子的预键入版本以供在应用中使用。这很重要,原因如下:

¥While it's possible to import the RootState and AppDispatch types into each component, it's better to create pre-typed versions of the useDispatch and useSelector hooks for usage in your application. This is important for a couple reasons:

  • 对于 useSelector,你无需每次都输入 (state: RootState)

    ¥For useSelector, it saves you the need to type (state: RootState) every time

  • 对于 useDispatch,默认的 Dispatch 类型不知道 thunk 或其他中间件。为了正确分派 thunk,你需要使用存储中包含 thunk 中间件类型的特定自定义 AppDispatch 类型,并将其与 useDispatch 一起使用。添加预先输入的 useDispatch 钩子可以防止你忘记在需要的地方导入 AppDispatch

    ¥For useDispatch, the default Dispatch type does not know about thunks or other middleware. In order to correctly dispatch thunks, you need to use the specific customized AppDispatch type from the store that includes the thunk middleware types, and use that with useDispatch. Adding a pre-typed useDispatch hook keeps you from forgetting to import AppDispatch where it's needed.

由于这些是实际变量,而不是类型,因此在单独的文件(例如 app/hooks.ts)中定义它们非常重要,而不是在存储设置文件中。这允许你将它们导入到任何需要使用钩子的组件文件中,并避免潜在的循环导入依赖问题。

¥Since these are actual variables, not types, it's important to define them in a separate file such as app/hooks.ts, not the store setup file. This allows you to import them into any component file that needs to use the hooks, and avoids potential circular import dependency issues.

.withTypes()

以前,"pre-typing" 钩子与应用设置的方法略有不同。结果类似于下面的代码片段:

¥Previously, the approach for "pre-typing" hooks with your app setting was a little varied. The result would look something like the snippet below:

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 方法。

¥React Redux v9.1.0 adds a new .withTypes method to each of these hooks, analogous to the .withTypes method found on Redux Toolkit's createAsyncThunk.

现在的设置变成:

¥The setup now becomes:

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>()

应用使用

¥Application Usage

定义切片状态和操作类型

¥Define Slice State and Action Types

每个切片文件应该为其初始状态值定义一个类型,以便 createSlice 可以正确推断每种情况 reducer 中 state 的类型。

¥Each slice file should define a type for its initial state value, so that createSlice can correctly infer the type of state in each case reducer.

所有生成的操作都应使用 Redux Toolkit 中的 PayloadAction<T> 类型定义,该类型将 action.payload 字段的类型作为其通用参数。

¥All generated actions should be defined using the PayloadAction<T> type from Redux Toolkit, which takes the type of the action.payload field as its generic argument.

你可以安全地从此处的存储文件导入 RootState 类型。这是一个循环导入,但 TypeScript 编译器可以正确处理类型。这对于编写选择器函数等用例可能是需要的。

¥You can safely import the RootState type from the store file here. It's a circular import, but the TypeScript compiler can correctly handle that for types. This may be needed for use cases like writing selector functions.

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 作为其参数。

¥The generated action creators will be correctly typed to accept a payload argument based on the PayloadAction<T> type you provided for the reducer. For example, incrementByAmount requires a number as its argument.

在某些情况下,TypeScript 可能会不必要地收紧初始状态的类型。如果发生这种情况,你可以通过使用 as 强制转换初始状态来解决此问题,而不是声明变量的类型:

¥In some cases, TypeScript may unnecessarily tighten the type of the initial state. If that happens, you can work around it by casting the initial state using as, instead of declaring the type of the variable:

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

在组件中使用类型化钩子

¥Use Typed Hooks in Components

在组件文件中,导入预先键入的钩子,而不是来自 React Redux 的标准钩子。

¥In component files, import the pre-typed hooks instead of the standard hooks from 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 可以帮助你的团队轻松导入正确的 hooks。当意外使用错误的导入时,typescript-eslint/no-restricted-imports 规则会显示警告。

¥ESLint can help your team import the right hooks easily. The typescript-eslint/no-restricted-imports rule can show a warning when the wrong import is used accidentally.

你可以将其添加到 ESLint 配置中作为示例:

¥You could add this to your ESLint config as an example:

"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 逻辑

¥Typing Additional Redux Logic

类型检查 reducer

¥Type Checking Reducers

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

¥Reducers are pure functions that receive the current state and incoming action as arguments, and return a new state.

如果你使用 Redux Toolkit 的 createSlice,你应该很少需要单独专门键入一个 reducer。如果你确实编写了一个独立的 reducer,通常声明 initialState 值的类型并将 action 键入为 UnknownAction 就足够了:

¥If you are using Redux Toolkit's createSlice, you should rarely need to specifically type a reducer separately. If you do actually write a standalone reducer, it's typically sufficient to declare the type of the initialState value, and type the action as 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> 类型。

¥However, the Redux core does export a Reducer<State, Action> type you can use as well.

类型检查中间件

¥Type Checking Middleware

中间件 是 Redux 存储的扩展机制。中间件组成一个管道,封装存储的 dispatch 方法,并可以访问存储的 dispatchgetState 方法。

¥Middleware are an extension mechanism for the Redux store. Middleware are composed into a pipeline that wrap the store's dispatch method, and have access to the store's dispatch and getState methods.

Redux 核心导出了 Middleware 类型,可用于正确键入中间件函数:

¥The Redux core exports a Middleware type that can be used to correctly type a middleware function:

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(调度)的通用参数:

¥A custom middleware should use the Middleware type, and pass the generic args for S (state) and D (dispatch) if needed:

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 存储类型,你应该禁用此行的规则并继续使用 {}

¥If you are using typescript-eslint, the @typescript-eslint/ban-types rule might report an error if you use {} for the dispatch value. The recommended changes it makes are incorrect and will break your Redux store types, you should disable the rule for this line and keep using {}.

仅当你在中间件内分派额外的 thunk 时,才可能需要分派泛型。

¥The dispatch generic should likely only be needed if you are dispatching additional thunks within the middleware.

在使用 type RootState = ReturnType<typeof store.getState> 的情况下,可以通过将 RootState 的类型定义切换为以下方式来避免使用 中间件和存储定义之间的循环类型引用

¥In cases where type RootState = ReturnType<typeof store.getState> is used, a circular type reference between the middleware and store definitions can be avoided by switching the type definition of RootState to:

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

使用 Redux Toolkit 示例切换 RootState 的类型定义:

¥Switching the type definition of RootState with Redux Toolkit example:

//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

¥Type Checking Redux Thunks

Redux Thunk 是用于编写与 Redux 存储交互的同步和异步逻辑的标准中间件。thunk 函数接收 dispatchgetState 作为其参数。Redux Thunk 有一个内置的 ThunkAction 类型,我们可以用它来定义这些参数的类型:

¥Redux Thunk is the standard middleware for writing sync and async logic that interacts with the Redux store. A thunk function receives dispatch and getState as its parameters. Redux Thunk has a built in ThunkAction type which we can use to define types for those arguments:

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 对应 EUnknownAction 对应 A

¥You will typically want to provide the R (return type) and S (state) generic arguments. Unfortunately, TS does not allow only providing some generic arguments, so the usual values for the other arguments are unknown for E and UnknownAction for 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 时使用该类型:

¥To reduce repetition, you might want to define a reusable AppThunk type once, in your store file, and then use that type whenever you write a thunk:

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

请注意,这假设 thunk 没有任何有意义的返回值。如果你的 thunk 返回一个 promise 并且你想要 分派 thunk 后使用返回的 Promise,你会想将其用作 AppThunk<Promise<SomeReturnType>>

¥Note that this assumes that there is no meaningful return value from the thunk. If your thunk returns a promise and you want to use the returned promise after dispatching the thunk, you'd want to use this as AppThunk<Promise<SomeReturnType>>.

提醒

不要忘记默认的 useDispatch 钩子不知道 thunk,因此调度 thunk 将导致类型错误。一定要 在组件中使用 Dispatch 的更新形式,将 thunk 识别为可接受的调度类型

¥Don't forget that the default useDispatch hook does not know about thunks, and so dispatching a thunk will cause a type error. Be sure to use an updated form of Dispatch in your components that recognizes thunks as an acceptable type to dispatch.

与 React Redux 一起使用

¥Usage with React Redux

虽然 反应还原 是一个独立于 Redux 本身的库,但它通常与 React 一起使用。

¥While React Redux is a separate library from Redux itself, it is commonly used with React.

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

¥For a complete guide on how to correctly use React Redux with TypeScript, see the "Static Typing" page in the React Redux docs. This section will highlight the standard patterns.

如果你使用 TypeScript,React Redux 类型在 DefinelyTyped 中单独维护,但作为 React-Redux 包的依赖包含在内,因此它们应该自动安装。如果你仍然需要手动安装它们,请运行:

¥If you are using TypeScript, the React Redux types are maintained separately in DefinitelyTyped, but included as a dependency of the react-redux package, so they should be installed automatically. If you still need to install them manually, run:

npm install @types/react-redux

键入 useSelector 钩子

¥Typing the useSelector hook

在选择器函数中声明 state 参数的类型,并且将推断 useSelector 的返回类型以匹配选择器的返回类型:

¥Declare the type of the state parameter in the selector function, and the return type of useSelector will be inferred to match the return type of the 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)

这也可以内联完成:

¥This can also be done inline as well:

const isOn = useSelector((state: RootState) => state.isOn)

但是,更喜欢创建一个预先键入的 useAppSelector 钩子,并使用正确类型的内置 state 来代替。

¥However, prefer creating a pre-typed useAppSelector hook with the correct type of state built-in instead.

键入 useDispatch 钩子

¥Typing the useDispatch hook

默认情况下,useDispatch 的返回值是 Redux 核心类型定义的标准 Dispatch 类型,因此不需要声明:

¥By default, the return value of useDispatch is the standard Dispatch type defined by the Redux core types, so no declarations are needed:

const dispatch = useDispatch()

但是,更喜欢创建一个预先键入的 useAppDispatch 钩子,并使用正确类型的内置 Dispatch 来代替。

¥However, prefer creating a pre-typed useAppDispatch hook with the correct type of Dispatch built-in instead.

输入 connect 高阶组件

¥Typing the connect higher order component

如果你还在使用 connect,你应该使用 @types/react-redux^7.1.2 导出的 ConnectedProps<T> 类型来自动从 connect 推断属性的类型。这需要将 connect(mapState, mapDispatch)(MyComponent) 调用分为两部分:

¥If you are still using connect, you should use the ConnectedProps<T> type exported by @types/react-redux^7.1.2 to infer the types of the props from connect automatically. This requires splitting the connect(mapState, mapDispatch)(MyComponent) call into two parts:

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 工具包一起使用

¥Usage with Redux Toolkit

使用 TypeScript 设置标准 Redux 工具包项目 部分已经涵盖了 configureStorecreateSlice 的正常使用模式,Redux 工具包 "与 TypeScript 一起使用" 页 部分详细涵盖了所有 RTK API。

¥The Standard Redux Toolkit Project Setup with TypeScript section already covered the normal usage patterns for configureStore and createSlice, and the Redux Toolkit "Usage with TypeScript" page covers all of the RTK APIs in detail.

以下是使用 RTK 时常见的一些其他类型模式。

¥Here are some additional typing patterns you will commonly see when using RTK.

键入 configureStore

¥Typing configureStore

configureStore 从提供的根 reducer 函数推断状态值的类型,因此不需要特定的类型声明。

¥configureStore infers the type of the state value from the provided root reducer function, so no specific type declarations should be needed.

如果你想向存储添加其他中间件,请务必使用 getDefaultMiddleware() 返回的数组中包含的专用 .concat().prepend() 方法,因为这些方法将正确保留你要添加的中间件的类型。(使用普通 JS 数组扩展通常会丢失这些类型。)

¥If you want to add additional middleware to the store, be sure to use the specialized .concat() and .prepend() methods included in the array returned by getDefaultMiddleware(), as those will correctly preserve the types of the middleware you're adding. (Using plain JS array spreads often loses those types.)

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)
})

匹配动作

¥Matching Actions

RTK 生成的动作创建器具有充当 类型谓词match 方法。调用 someActionCreator.match(action) 将与 action.type 字符串进行字符串比较,如果用作条件,则将 action 的类型缩小为正确的 TS 类型:

¥RTK-generated action creators have a match method that acts as a type predicate. Calling someActionCreator.match(action) will do a string comparison against the action.type string, and if used as a condition, narrow the type of action down to be the correct TS type:

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 方法。

¥This is particularly useful when checking for action types in Redux middleware, such as custom middleware, redux-observable, and RxJS's filter method.

键入 createSlice

¥Typing createSlice

定义单独的 Case 缩减器

¥Defining Separate Case Reducers

如果你有太多 case 缩减器并且内联定义它们会很混乱,或者你想跨切片重用 case 缩减器,你还可以在 createSlice 调用之外定义它们并将它们键入为 CaseReducer

¥If you have too many case reducers and defining them inline would be messy, or you want to reuse case reducers across slices, you can also define them outside the createSlice call and type them as CaseReducer:

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

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

键入 extraReducers

¥Typing extraReducers

如果要在 createSlice 中添加 extraReducers 字段,请务必使用 "构建器回调" 表单,因为 "普通对象" 表单无法正确推断操作类型。将 RTK 生成的动作创建器传递给 builder.addCase() 将正确推断 action 的类型:

¥If you are adding an extraReducers field in createSlice, be sure to use the "builder callback" form, as the "plain object" form cannot infer action types correctly. Passing an RTK-generated action creator to builder.addCase() will correctly infer the type of the 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 回调

¥Typing prepare Callbacks

如果要向操作添加 metaerror 属性,或者自定义操作的 payload,则必须使用 prepare 表示法来定义大小写缩减器。在 TypeScript 中使用此表示法如下所示:

¥If you want to add a meta or error property to your action, or customize the payload of your action, you have to use the prepare notation for defining the case reducer. Using this notation with TypeScript looks like:

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 } }
}
}
}
})

修复导出切片中的圆形类型

¥Fixing Circular Types in Exported Slices

最后,在极少数情况下,你可能需要导出特定类型的切片 reducer,以解决循环类型依赖问题。这可能看起来像:

¥Finally, on rare occasions you might need to export the slice reducer with a specific type in order to break a circular type dependency problem. This might look like:

export default counterSlice.reducer as Reducer<Counter>

键入 createAsyncThunk

¥Typing createAsyncThunk

对于基本用法,你需要为 createAsyncThunk 提供的唯一类型是负载创建回调的单个参数的类型。你还应该确保回调的返回值输入正确:

¥For basic usage, the only type you need to provide for createAsyncThunk is the type of the single argument for your payload creation callback. You should also ensure that the return value of the callback is typed correctly:

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 参数字段":

¥If you need to modify the types of the thunkApi parameter, such as supplying the type of the state returned by getState(), you must supply the first two generic arguments for return type and payload argument, plus whichever "thunkApi argument fields" are relevant in an object:

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

¥Typing createEntityAdapter

createEntityAdapter 与 Typescript 的使用取决于你的实体是否由 id 属性规范化,或者是否需要自定义 selectId

¥Usage of createEntityAdapter with Typescript varies based on whether your entities are normalized by an id property, or whether a custom selectId is needed.

如果你的实体由 id 属性规范化,则 createEntityAdapter 仅要求你将实体类型指定为单个通用参数。例如:

¥If your entities are normalized by an id property, createEntityAdapter only requires you to specify the entity type as the single generic argument. For example:

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 的类型,而不必手动提供。

¥On the other hand, if the entity needs to be normalized by a different property, we instead recommend passing a custom selectId function and annotating there. This allows proper inference of the ID's type, instead of having to provide it manually.

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)
}
}
})

附加建议

¥Additional Recommendations

使用 React Redux Hooks API

¥Use the React Redux Hooks API

我们建议使用 React Redux hooks API 作为默认方法。hooks API 与 TypeScript 一起使用要简单得多,因为 useSelector 是一个简单的钩子,它采用选择器函数,并且可以从 state 参数的类型轻松推断出返回类型。

¥We recommend using the React Redux hooks API as the default approach. The hooks API is much simpler to use with TypeScript, as useSelector is a simple hook that takes a selector function, and the return type is easily inferred from the type of the state argument.

虽然 connect 仍然可以正常工作并且可以打字,但正确打字要困难得多。

¥While connect still works fine, and can be typed, it's much more difficult to type correctly.

避免行动类型联合

¥Avoid Action Type Unions

我们特别建议不要尝试创建操作类型的联合,因为它没有提供任何真正的好处,并且实际上在某些方面误导了编译器。请参阅 RTK 维护者 Lenz Weber 的帖子 不要使用 Redux 操作类型创建联合类型,了解为什么这是一个问题。

¥We specifically recommend against trying to create unions of action types, as it provides no real benefit and actually misleads the compiler in some ways. See RTK maintainer Lenz Weber's post Do Not Create Union Types with Redux Action Types for an explanation of why this is a problem.

此外,如果你使用的是 createSlice,则你已经知道该切片定义的所有操作都已正确处理。

¥In addition, if you're using createSlice, you already know that all actions defined by that slice are being handled correctly.

资源

¥Resources

有关更多信息,请参阅以下附加资源:

¥For further information, see these additional resources: