与 TypeScript 一起使用
- 如何使用每个 Redux Toolkit API 与 TypeScript 的详细信息
简介
Redux Toolkit 是用 TypeScript 编写的,其 API 的设计使其能够与 TypeScript 应用程序进行很好的集成。
此页面为 Redux Toolkit 中包含的不同 API 提供了具体的详细信息,以及如何使用 TypeScript 正确地对它们进行类型化。
请查看 TypeScript 快速开始教程页面 ,了解如何设置和使用 Redux Toolkit 和 React Redux 与 TypeScript 一起工作的简要概述。
如果你遇到了本页面未描述的类型问题,请开启一个问题进行讨论。
configureStore
在 TypeScript 快速开始教程页面 中展示了使用 configureStore
的基础知识。这里有一些你可能会发现有用的额外细节。
获取 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
类型
如果你想从你的 store 中获取 Dispatch
类型,你可以在创建 store 后提取它。建议给类型一个不同的名字,如 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 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(
// 正确类型化的中间件可以直接使用
additionalMiddleware,
// 你也可以手动类型化中间件
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>,
)
// prepend 和 concat 调用可以被链式调用
.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),
})
使用提取的 Dispatch
类型与 React Redux
默认情况下,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
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
})
.addCase(decrement, (state, action: PayloadAction<string>) => {
// 这将会报错
}),
)
类型化 builder.addMatcher
作为 builder.addMatcher
的第一个 matcher
参数,应使用一个 类型谓词 函数。
因此,第二个 reducer
参数的 action
参数可以由 TypeScript 推断出来:
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,
},
})
// 现在可用:
slice.actions.increment(2)
// 也可用:
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 }
// 第一种方法:使用该类型定义初始状态
const initialState: SliceState = { state: 'loading' }
createSlice({
name: 'test1',
initialState, // 切片状态的类型 SliceState 被推断出来
reducers: {},
})
// 或者,根据需要转换初始状态
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 } }
},
},
},
})
为切片生成的动作类型
createSlice
通过将切片的 name
字段与 reducer 函数的字段名组合,生成动作类型字符串,如 'test/increment'
。这是强类型的,准确的值,得益于 TS 的字符串字面量分析。
你也可以使用 slice.action.myAction.match
类型谓词,它将动作对象缩小到精确的类型:
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})
type incrementType = typeof slice.actions.increment.type
// 类型 incrementType = 'test/increment'
function myCustomMiddleware(action: Action) {
if (slice.actions.increment.match(action)) {
// 在这里,`action` 被缩小到类型 `PayloadAction<number>`。
}
}
如果你实际上 需要 那种类型,不幸的是,除了手动转换之外,没有其他方法。
使用 extraReducers
的类型安全
将动作 type
字符串映射到 reducer 函数的 reducer 查找表不容易完全类型化正确。这影响了 createReducer
和 createSlice
的 extraReducers
参数。所以,像使用 createReducer
一样,你应该使用 "builder 回调" 方法 来定义 reducer 对象参数。
当一个切片 reducer 需要处理其他切片生成的动作类型,或者由 createAction
的特定调用生成的动作(如由 createAsyncThunk
生成的动作)时,这特别有用。
const fetchUserById = createAsyncThunk(
'users/fetchById',
// 如果你在这里类型化你的函数参数
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: {
// 在这里填写主要逻辑
},
extraReducers: (builder) => {
builder.addCase(fetchUserById.pending, (state, action) => {
// 现在,`state` 和 `action` 都根据切片状态和 `pending` 动作创建器正确地类型化了
})
},
})
像 createReducer
中的 builder
一样,这个 builder
也接受 addMatcher
(参见 typing builder.matcher
)和 addDefaultCase
。