跳至主要內容

使用 TypeScript

您將學到什麼
  • 有關如何使用 TypeScript 搭配每個 Redux Toolkit API 的詳細資訊

簡介

Redux Toolkit 是使用 TypeScript 編寫的,其 API 設計為能與 TypeScript 應用程式進行極佳整合。

此頁面提供 Redux Toolkit 中包含的每個不同 API 的具體詳細資訊,以及如何使用 TypeScript 正確輸入這些 API。

請參閱 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 選項推斷。因此,如果您加入類型正確的 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 參數,應使用 類型謂詞 函式。因此,第二個 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,
},
})
// now available:
slice.actions.increment(2)
// also available:
slice.caseReducers.increment(0, { type: 'increment', payload: 5 })

如果您有太多案例 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,
},
})

定義初始狀態類型

您可能已經注意到,將 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 } }
},
},
},
})

切片的動作類型產生

createSlice 會將 slice 中的 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
// 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 的類型安全性

將動作 type 字串對應到 reducer 函式的 reducer 查詢表不容易完全正確地型別。這會影響 createReducercreateSliceextraReducers 參數。因此,就像使用 createReducer 一樣,你應該使用「建構器回呼」方法來定義 reducer 物件參數。

當 slice reducer 需要處理其他 slice 產生的動作類型,或由特定呼叫 createAction 產生的動作類型(例如由 createAsyncThunk 產生的動作)時,這特別有用。

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

所有欄位都是選用的 Payload

如果你嘗試提供所有欄位都是選用的 payload 類型,例如 PayloadAction<Partial<User>>PayloadAction<{value?: string}>,TS 可能無法正確推斷動作類型。

你可以透過 使用自訂 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 相同,只有一個主要差異。

無法將 state 和/或 dispatch 的類型提供為 ThunkApiConfig 的一部分,因為這會導致循環類型。

相反地,需要時宣告類型 - 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 函式。這也可以使用 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 的回傳類型也會反映在所有產生的動作類型中。

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)。這允許你在 reducer 中以及在發送 createAsyncThunk 動作後在元件中參照錯誤 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 也是如此 - 如果你不需要存取任何潛在的錯誤 payload,你可以忽略它。

此外,你可以利用 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 取代原始的 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)
})
},
})