跳至主要内容

使用 TypeScript

您将学到什么
  • 有关如何使用每个 Redux Toolkit API 与 TypeScript 的详细信息

介绍

Redux Toolkit 是用 TypeScript 编写的,其 API 旨在实现与 TypeScript 应用程序的出色集成。

本页提供了 Redux Toolkit 中包含的每个不同 API 的具体细节,以及如何使用 TypeScript 正确地对其进行类型化。

请参阅 TypeScript 快速入门教程页面,了解有关如何设置和使用 Redux Toolkit 和 React Redux 与 TypeScript 协同工作的简要概述.

信息

如果您遇到此页面未描述的类型问题,请打开一个问题进行讨论。

configureStore

使用configureStore的基本方法在TypeScript 快速入门教程页面中展示。这里有一些您可能会发现有用的额外细节。

获取State类型

获取State类型最简单的方法是在预先定义根 reducer 并提取其ReturnType。建议给类型一个不同的名称,例如RootState,以防止混淆,因为类型名称State通常被过度使用。

import { combineReducers } from '@reduxjs/toolkit'
const rootReducer = combineReducers({})
export type RootState = ReturnType<typeof rootReducer>

或者,如果您选择不自己创建rootReducer,而是直接将切片 reducer 传递给configureStore(),则需要稍微修改类型以正确推断根 reducer。

import { configureStore } from '@reduxjs/toolkit'
// ...
const store = configureStore({
reducer: {
one: oneSlice.reducer,
two: twoSlice.reducer,
},
})
export type RootState = ReturnType<typeof store.getState>

export default store

如果您将 reducer 直接传递给configureStore()并且没有显式定义根 reducer,则没有对rootReducer的引用。相反,您可以参考store.getState,以获取State类型。

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './rootReducer'
const store = configureStore({
reducer: rootReducer,
})
export type RootState = ReturnType<typeof store.getState>

获取Dispatch类型

如果您想从您的商店获取Dispatch类型,您可以在创建商店后提取它。建议给类型一个不同的名称,例如AppDispatch,以防止混淆,因为类型名称Dispatch通常被过度使用。您可能还会发现导出一个类似于useAppDispatch的钩子更方便,如下所示,然后在您调用useDispatch的任何地方使用它。

import { configureStore } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'
import rootReducer from './rootReducer'

const store = configureStore({
reducer: rootReducer,
})

export type AppDispatch = typeof store.dispatch
export const useAppDispatch = useDispatch.withTypes<AppDispatch>() // Export a hook that can be reused to resolve types

export default store

Dispatch类型的正确类型

dispatch函数类型的类型将直接从middleware选项推断。因此,如果您添加了类型正确的中间件,dispatch应该已经被正确地类型化了。

由于 TypeScript 在使用扩展运算符组合数组时通常会扩展数组类型,我们建议使用getDefaultMiddleware()返回的Tuple.concat(...).prepend(...)方法。

import { configureStore } from '@reduxjs/toolkit'
import additionalMiddleware from 'additional-middleware'
import logger from 'redux-logger'
// @ts-ignore
import untypedMiddleware from 'untyped-middleware'
import rootReducer from './rootReducer'

export type RootState = ReturnType<typeof rootReducer>
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),
})

export type AppDispatch = typeof store.dispatch

export default store

在没有getDefaultMiddleware的情况下使用Tuple

如果您想完全跳过使用getDefaultMiddleware,则需要使用Tuple来安全地创建您的middleware数组。此类扩展了默认的 JavaScript Array 类型,只是修改了.concat(...)的类型,并添加了.prepend(...)方法。

例如

import { configureStore, Tuple } from '@reduxjs/toolkit'

configureStore({
reducer: rootReducer,
middleware: () => new Tuple(additionalMiddleware, logger),
})

使用 React Redux 中提取的Dispatch类型

默认情况下,React Redux 的useDispatch钩子不包含任何考虑中间件的类型。如果您需要在调度时为dispatch函数指定更具体的类型,您可以指定返回的dispatch函数的类型,或者创建useSelector的自定义类型版本。有关详细信息,请参阅React Redux 文档

createAction

对于大多数用例,不需要对action.type进行字面定义,因此可以使用以下方法

createAction<number>('test')

这将导致创建的操作类型为PayloadActionCreator<number, string>

在某些设置中,您将需要action.type的字面类型。不幸的是,TypeScript 类型定义不允许混合手动定义和推断的类型参数,因此您必须在泛型定义和实际 JavaScript 代码中都指定type

createAction<number, 'test'>('test')

如果您正在寻找一种无需重复的替代方法,可以使用准备回调,以便可以从参数推断出两个类型参数,从而无需指定操作类型。

function withPayloadType<T>() {
return (t: T) => ({ payload: t })
}
createAction('test', withPayloadType<string>())

使用字面类型action.type的替代方法

如果您将action.type用作区分联合的区分符,例如在case语句中正确地对您的有效负载进行类型化,那么您可能会对这种替代方法感兴趣

创建的操作创建者具有一个match方法,该方法充当类型谓词

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

match方法与redux-observable和 RxJS 的filter方法结合使用也非常有用。

createReducer

构建类型安全的 Reducer 参数对象

createReducer 的第二个参数是一个回调函数,它接收一个 ActionReducerMapBuilder 实例

const increment = createAction<number, 'increment'>('increment')
const decrement = createAction<number, 'decrement'>('decrement')
createReducer(0, (builder) =>
builder
.addCase(increment, (state, action) => {
// action is inferred correctly here
})
.addCase(decrement, (state, action: PayloadAction<string>) => {
// this would error out
}),
)

builder.addMatcher 添加类型

作为 builder.addMatcher 的第一个 matcher 参数,应该使用一个 类型谓词 函数。这样,TypeScript 就可以推断出第二个 reducer 参数的 action 参数的类型

function isNumberValueAction(action: UnknownAction): action is PayloadAction<{ value: number }> {
return typeof action.payload.value === 'number'
}

createReducer({ value: 0 }, builder =>
builder.addMatcher(isNumberValueAction, (state, action) => {
state.value += action.payload.value
})
})

createSlice

由于 createSlice 会为您创建操作和 reducer,因此您无需担心类型安全问题。操作类型可以直接内联提供

const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})
// now available:
slice.actions.increment(2)
// also available:
slice.caseReducers.increment(0, { type: 'increment', payload: 5 })

如果您有太多 case reducer,并且内联定义它们会很混乱,或者您想在多个 slice 中重用 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,
},
})

定义初始状态类型

您可能已经注意到,将 SliceState 类型作为泛型传递给 createSlice 不是一个好主意。这是因为在几乎所有情况下,createSlice 的后续泛型参数都需要推断,而 TypeScript 无法在同一个“泛型块”中混合显式声明和推断泛型类型。

标准方法是为您的状态声明一个接口或类型,创建一个使用该类型的初始状态值,并将初始状态值传递给 createSlice。您也可以使用 initialState: myInitialState satisfies SliceState as SliceState 结构。

type SliceState = { state: 'loading' } | { state: 'finished'; data: string }

// First approach: define the initial state using that type
const initialState: SliceState = { state: 'loading' }

createSlice({
name: 'test1',
initialState, // type SliceState is inferred for the state of the slice
reducers: {},
})

// Or, cast the initial state as necessary
createSlice({
name: 'test2',
initialState: { state: 'loading' } satisfies SliceState as SliceState,
reducers: {},
})

这将产生一个 Slice<SliceState, ...>

使用 prepare 回调定义操作内容

如果您想向操作添加 metaerror 属性,或者自定义操作的 payload,则必须使用 prepare 符号。

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

切片生成的 Action 类型

createSlice 通过将切片的 name 字段与 reducer 函数的字段名称组合起来生成 action 类型字符串,例如 'test/increment'。由于 TS 的字符串字面量分析,这被强类型化为确切的值。

您还可以使用 slice.action.myAction.match 类型谓词,它将缩小 action 对象到确切类型

const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})

type incrementType = typeof slice.actions.increment.type
// type incrementType = 'test/increment'

function myCustomMiddleware(action: Action) {
if (slice.actions.increment.match(action)) {
// `action` is narrowed down to the type `PayloadAction<number>` here.
}
}

如果您确实需要该类型,不幸的是,除了手动转换之外别无他法。

使用 extraReducers 的类型安全

将 action type 字符串映射到 reducer 函数的 reducer 查找表不容易完全正确地类型化。这会影响 createReducercreateSliceextraReducers 参数。因此,与 createReducer 一样,您应该使用“构建器回调”方法 来定义 reducer 对象参数。

当切片 reducer 需要处理由其他切片生成的 action 类型,或由对 createAction 的特定调用生成的 action 类型(例如由 createAsyncThunk 生成的 action)时,这尤其有用。

const fetchUserById = createAsyncThunk(
'users/fetchById',
// if you type your function argument here
async (userId: number) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`)
return (await response.json()) as Returned
},
)

interface UsersState {
entities: User[]
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}

const initialState = {
entities: [],
loading: 'idle',
} satisfies UsersState as UsersState

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

createReducer 中的 builder 一样,此 builder 也接受 addMatcher(请参阅 类型化 builder.matcher)和 addDefaultCase

具有所有可选字段的有效负载

如果您尝试提供一个所有字段都是可选的有效负载类型,例如 PayloadAction<Partial<User>>PayloadAction<{value?: string}>,TS 可能无法正确推断 action 类型。

您可以通过 使用自定义 AtLeastOne 实用程序类型 来解决此问题,以帮助确保必须传入至少一个字段

type AtLeastOne<T extends Record<string, any>> = keyof T extends infer K
? K extends string
? Pick<T, K & keyof T> & Partial<T>
: never
: never

// Use this type instead of `Partial<MyPayloadType>`
type AtLeastOneUserField = AtLeastOne<User>

createSlice 中键入异步 Thunk

从 2.0 开始,createSlice 允许 使用回调语法在 reducers 中定义 thunk

create.asyncThunk 方法的类型推断与 createAsyncThunk 相同,但有一个关键区别。

由于会造成循环类型,因此不能在 ThunkApiConfig 中提供 statedispatch 的类型。

相反,需要在需要时断言类型 - getState() as RootState。您也可以为 payload 函数显式指定返回类型,以打破循环类型推断周期。

create.asyncThunk<Todo, string, { rejectValue: { error: string } }>(
// may need to include an explicit return type
async (id: string, thunkApi): Promise<Todo> => {
// Cast types for `getState` and `dispatch` manually
const state = thunkApi.getState() as RootState
const dispatch = thunkApi.dispatch as AppDispatch
try {
const todo = await fetchTodo()
return todo
} catch (e) {
throw thunkApi.rejectWithValue({
error: 'Oh no!',
})
}
},
)

对于常见的 thunk API 配置选项,提供了一个 withTypes 辅助函数

reducers: (create) => {
const createAThunk = create.asyncThunk.withTypes<{
rejectValue: { error: string }
}>()

return {
fetchTodo: createAThunk<Todo, string>(async (id, thunkApi) => {
throw thunkApi.rejectWithValue({
error: 'Oh no!',
})
}),
fetchTodos: createAThunk<Todo[], string>(async (id, thunkApi) => {
throw thunkApi.rejectWithValue({
error: 'Oh no, not again!',
})
}),
}
}

包装 createSlice

如果您需要重用 reducer 逻辑,通常会编写 "高阶 reducer",这些 reducer 使用额外的通用行为包装 reducer 函数。这也可以使用 createSlice 完成,但由于 createSlice 的类型复杂,您必须以非常特定的方式使用 SliceCaseReducersValidateSliceCaseReducers 类型。

以下是一个这样的“通用”包装 createSlice 调用的示例

interface GenericState<T> {
data?: T
status: 'loading' | 'finished' | 'error'
}

const createGenericSlice = <
T,
Reducers extends SliceCaseReducers<GenericState<T>>,
>({
name = '',
initialState,
reducers,
}: {
name: string
initialState: GenericState<T>
reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>
}) => {
return createSlice({
name,
initialState,
reducers: {
start(state) {
state.status = 'loading'
},
/**
* If you want to write to values of the state that depend on the generic
* (in this case: `state.data`, which is T), you might need to specify the
* State type manually here, as it defaults to `Draft<GenericState<T>>`,
* which can sometimes be problematic with yet-unresolved generics.
* This is a general problem when working with immer's Draft type and generics.
*/
success(state: GenericState<T>, action: PayloadAction<T>) {
state.data = action.payload
state.status = 'finished'
},
...reducers,
},
})
}

const wrappedSlice = createGenericSlice({
name: 'test',
initialState: { status: 'loading' } as GenericState<string>,
reducers: {
magic(state) {
state.status = 'finished'
state.data = 'hocus pocus'
},
},
})

createAsyncThunk

基本 createAsyncThunk 类型

在最常见的用例中,您无需为 createAsyncThunk 调用本身显式声明任何类型。

只需为 payloadCreator 参数的第一个参数提供一个类型,就像您为任何函数参数所做的那样,生成的 thunk 将接受相同的类型作为其输入参数。payloadCreator 的返回类型也将反映在所有生成的 action 类型中。

interface MyData {
// ...
}

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 对象指定类型

payloadCreator 的第二个参数,称为 thunkApi,是一个对象,其中包含对来自 thunk 中间件的 dispatchgetStateextra 参数的引用,以及一个名为 rejectWithValue 的实用函数。如果您想在 payloadCreator 中使用这些参数,则需要定义一些泛型参数,因为这些参数的类型无法推断。此外,由于 TS 无法混合显式和推断的泛型参数,从现在开始,您也必须定义 ReturnedThunkArg 泛型参数。

手动定义 thunkApi 类型

要为这些参数定义类型,请将一个对象作为第三个泛型参数传递,其中包含一些或所有这些字段的类型声明

type AsyncThunkConfig = {
/** return type for `thunkApi.getState` */
state?: unknown
/** type for `thunkApi.dispatch` */
dispatch?: Dispatch
/** type of the `extra` argument for the thunk middleware, which will be passed in as `thunkApi.extra` */
extra?: unknown
/** type to be passed into `rejectWithValue`'s first argument that will end up on `rejectedAction.payload` */
rejectValue?: unknown
/** return type of the `serializeError` option callback */
serializedErrorType?: unknown
/** type to be returned from the `getPendingMeta` option callback & merged into `pendingAction.meta` */
pendingMeta?: unknown
/** type to be passed into the second argument of `fulfillWithValue` to finally be merged into `fulfilledAction.meta` */
fulfilledMeta?: unknown
/** type to be passed into the second argument of `rejectWithValue` to finally be merged into `rejectedAction.meta` */
rejectedMeta?: unknown
}
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
})

如果您正在执行一个通常会成功或具有预期错误格式的请求,您可以将一个类型传递给 rejectValuereturn rejectWithValue(knownPayload) 在 action 创建器中。这允许您在 reducer 中以及在分派 createAsyncThunk action 后在组件中引用错误 payload。

interface MyKnownError {
errorMessage: string
// ...
}
interface UserAttributes {
id: string
first_name: string
last_name: string
email: string
}

const updateUser = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
UserAttributes,
// Types for ThunkAPI
{
extra: {
jwt: string
}
rejectValue: MyKnownError
}
>('users/update', async (user, thunkApi) => {
const { id, ...userData } = user
const response = await fetch(`https://reqres.in/api/users/${id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${thunkApi.extra.jwt}`,
},
body: JSON.stringify(userData),
})
if (response.status === 400) {
// Return the known error for future handling
return thunkApi.rejectWithValue((await response.json()) as MyKnownError)
}
return (await response.json()) as MyData
})

虽然这种 statedispatchextrarejectValue 的表示法乍一看可能不常见,但它允许你只提供你实际需要的这些类型的类型 - 例如,如果你在 payloadCreator 中不访问 getState,则无需为 state 提供类型。对于 rejectValue 也是如此 - 如果你不需要访问任何潜在的错误负载,你可以忽略它。

此外,你可以利用 createAction 提供的针对 action.payloadmatch 的检查作为类型保护,用于当你想要访问已定义类型上的已知属性时。示例

  • 在 reducer 中
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: {},
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
state.error = action.payload.errorMessage
} else {
state.error = action.error
}
})
},
})
  • 在组件中
const handleUpdateUser = async (userData) => {
const resultAction = await dispatch(updateUser(userData))
if (updateUser.fulfilled.match(resultAction)) {
const user = resultAction.payload
showToast('success', `Updated ${user.name}`)
} else {
if (resultAction.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
// Note: this would also be a good place to do any handling that relies on the `rejectedWithValue` payload, such as setting field errors
showToast('error', `Update failed: ${resultAction.payload.errorMessage}`)
} else {
showToast('error', `Update failed: ${resultAction.error.message}`)
}
}
}

定义预类型化的 createAsyncThunk

从 RTK 1.9 开始,你可以定义一个“预类型化”版本的 createAsyncThunk,它可以内置 statedispatchextra 的类型。这让你可以一次性设置这些类型,这样你就不必每次调用 createAsyncThunk 时都重复它们。

为此,请调用 createAsyncThunk.withTypes<>(),并传入一个包含上面列出的 AsyncThunkConfig 类型中任何字段的字段名称和类型的对象。这可能看起来像

const createAppAsyncThunk = createAsyncThunk.withTypes<{
state: RootState
dispatch: AppDispatch
rejectValue: string
extra: { s: string; n: number }
}>()

导入并使用那个预类型化的 createAppAsyncThunk 而不是原始的,类型将自动使用。

createEntityAdapter

createEntityAdapter 键入只需要你将实体类型指定为单个泛型参数。

createEntityAdapter 文档中的示例在 TypeScript 中看起来像这样

interface Book {
bookId: number
title: string
// ...
}

const booksAdapter = createEntityAdapter<Book>({
selectId: (book) => book.bookId,
sortComparer: (a, b) => a.title.localeCompare(b.title),
})

const booksSlice = createSlice({
name: 'books',
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
booksAdapter.setAll(state, action.payload.books)
},
},
})

createEntityAdapternormalizr 一起使用

当使用像 normalizr 这样的库时,你规范化的数据将类似于这种形状

{
result: 1,
entities: {
1: { id: 1, other: 'property' },
2: { id: 2, other: 'property' }
}
}

方法 addManyupsertManysetAll 都允许你直接传入此部分的 entities,无需额外的转换步骤。但是,normalizr 的 TS 类型定义目前没有正确反映结果中可能包含多个数据类型,因此你需要自己指定该类型结构。

以下是如何实现的示例

type Author = { id: number; name: string }
type Article = { id: number; title: string }
type Comment = { id: number; commenter: number }

export const fetchArticle = createAsyncThunk(
'articles/fetchArticle',
async (id: number) => {
const data = await fakeAPI.articles.show(id)
// Normalize the data so reducers can responded to a predictable payload.
// Note: at the time of writing, normalizr does not automatically infer the result,
// so we explicitly declare the shape of the returned normalized data as a generic arg.
const normalized = normalize<
any,
{
articles: { [key: string]: Article }
users: { [key: string]: Author }
comments: { [key: string]: Comment }
}
>(data, articleEntity)
return normalized.entities
},
)

export const slice = createSlice({
name: 'articles',
initialState: articlesAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// The type signature on action.payload matches what we passed into the generic for `normalize`, allowing us to access specific properties on `payload.articles` if desired
articlesAdapter.upsertMany(state, action.payload.articles)
})
},
})