使用 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
回调定义操作内容
如果您想向操作添加 meta
或 error
属性,或者自定义操作的 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 查找表不容易完全正确地类型化。这会影响 createReducer
和 createSlice
的 extraReducers
参数。因此,与 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
中提供 state
或 dispatch
的类型。
相反,需要在需要时断言类型 - 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
的类型复杂,您必须以非常特定的方式使用 SliceCaseReducers
和 ValidateSliceCaseReducers
类型。
以下是一个这样的“通用”包装 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 中间件的 dispatch
、getState
和 extra
参数的引用,以及一个名为 rejectWithValue
的实用函数。如果您想在 payloadCreator
中使用这些参数,则需要定义一些泛型参数,因为这些参数的类型无法推断。此外,由于 TS 无法混合显式和推断的泛型参数,从现在开始,您也必须定义 Returned
和 ThunkArg
泛型参数。
手动定义 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
})
如果您正在执行一个通常会成功或具有预期错误格式的请求,您可以将一个类型传递给 rejectValue
并 return 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
})
虽然这种 state
、dispatch
、extra
和 rejectValue
的表示法乍一看可能不常见,但它允许你只提供你实际需要的这些类型的类型 - 例如,如果你在 payloadCreator
中不访问 getState
,则无需为 state
提供类型。对于 rejectValue
也是如此 - 如果你不需要访问任何潜在的错误负载,你可以忽略它。
此外,你可以利用 createAction
提供的针对 action.payload
和 match
的检查作为类型保护,用于当你想要访问已定义类型上的已知属性时。示例
- 在 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
,它可以内置 state
、dispatch
和 extra
的类型。这让你可以一次性设置这些类型,这样你就不必每次调用 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)
},
},
})
将 createEntityAdapter
与 normalizr
一起使用
当使用像 normalizr
这样的库时,你规范化的数据将类似于这种形状
{
result: 1,
entities: {
1: { id: 1, other: 'property' },
2: { id: 2, other: 'property' }
}
}
方法 addMany
、upsertMany
和 setAll
都允许你直接传入此部分的 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)
})
},
})