跳至主要內容

使用 TypeScript

您將學到什麼
  • 使用 TypeScript 設定 Redux 應用程式的標準模式
  • 正確輸入 Redux 邏輯部分的技術
先備條件

概觀

TypeScript 是 JavaScript 的類型化超集,提供原始碼的編譯時間檢查。與 Redux 搭配使用時,TypeScript 可以協助提供

  1. 簡化器、狀態和動作建立器以及 UI 元件的類型安全
  2. 輕鬆重新整理類型化程式碼
  3. 團隊環境中的優越開發人員體驗

我們強烈建議在 Redux 應用程式中使用 TypeScript。不過,與所有工具一樣,TypeScript 也有取捨。它在撰寫額外程式碼、了解 TS 語法和建置應用程式方面增加了複雜性。同時,它透過在開發早期發現錯誤、啟用更安全且更有效率的重新整理,並作為現有原始碼的說明文件,提供價值。

我們相信務實使用 TypeScript 提供的價值和好處遠遠超過額外負擔,特別是在較大的程式碼庫中,但您應該花時間評估取捨,並決定是否值得在您自己的應用程式中使用 TS

有多種可能的方法來檢查 Redux 程式碼的類型。此頁面顯示我們建議的標準模式,以將 Redux 和 TypeScript 搭配使用,而不是詳盡的指南。遵循這些模式應能獲得良好的 TS 使用體驗,在類型安全和必須新增到程式碼庫的類型宣告數量之間取得最佳平衡

使用 TypeScript 的標準 Redux Toolkit 專案設定

我們假設一個典型的 Redux 專案同時使用 Redux Toolkit 和 React Redux。

Redux Toolkit (RTK) 是撰寫現代 Redux 邏輯的標準方法。RTK 已以 TypeScript 撰寫,其 API 的設計旨在提供良好的 TypeScript 使用體驗。

React Redux 的類型定義位於 NPM 上的個別 @types/react-redux typedefs 套件 中。除了對函式庫函式進行類型化之外,這些類型還匯出一些輔助工具,讓您更容易在 Redux 儲存和 React 元件之間撰寫類型安全的介面。

自 React Redux v7.2.3 起,react-redux 套件已相依於 @types/react-redux,因此類型定義將隨函式庫自動安裝。否則,您需要手動安裝它們(通常為 npm install @types/react-redux)。

適用於 Create-React-App 的 Redux+TS 範本 附帶已設定好這些模式的範例。

定義根狀態和派送類型

使用 configureStore 不需要任何額外的類型化。不過,您會想要擷取 RootState 類型和 Dispatch 類型,以便視需要參照它們。從儲存本身推論這些類型表示,當您新增更多狀態片段或修改中間件設定時,它們會正確更新。

由於這些是類型,因此可以安全地從您的儲存設定檔(例如 app/store.ts)直接匯出它們,並直接匯入其他檔案。

app/store.ts
import { configureStore } from '@reduxjs/toolkit'
// ...

export const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer
}
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

定義類型化 Hook

雖然可以將 RootStateAppDispatch 類型匯入每個元件,但最好為您的應用程式建立 useDispatchuseSelector Hook 的預先類型化版本。這很重要,原因有幾個

  • 對於 useSelector,它讓您每次都省去輸入 (state: RootState) 的麻煩
  • 對於 useDispatch,預設的 Dispatch 類型不了解 thunk 或其他中間件。為了正確派送 thunk,您需要使用儲存中包含 thunk 中間件類型的特定自訂 AppDispatch 類型,並將其與 useDispatch 一起使用。新增預先類型化的 useDispatch Hook 可讓您不會忘記在需要的地方匯入 AppDispatch

由於這些是實際變數,而非類型,因此務必在獨立檔案中定義它們,例如 app/hooks.ts,而非儲存設定檔。這可讓您將它們匯入需要使用掛勾的任何元件檔,並避免潛在的環狀匯入相依性問題。

.withTypes()

先前,使用應用程式設定「預先輸入」掛勾的方法有些許不同。結果會類似於下列程式碼片段

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

設定現已變為

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

應用程式使用

定義區段狀態和動作類型

每個區段檔都應為其初始狀態值定義一個類型,以便 createSlice 能正確推斷每個案例簡化器中 state 的類型。

所有產生的動作都應使用 Redux Toolkit 中的 PayloadAction<T> 類型定義,其將 action.payload 欄位的類型作為其一般引數。

您可以在這裡安全地從儲存檔匯入 RootState 類型。這是一個環狀匯入,但 TypeScript 編譯器可以正確地處理類型。這對於撰寫選擇器函式等使用案例可能是必要的。

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

產生的動作建立器將正確地輸入以接受 payload 引數,該引數根據您為簡化器提供的 PayloadAction<T> 類型而定。例如,incrementByAmount 需要一個 number 作為其引數。

在某些情況下,TypeScript 可能會不必要地收緊初始狀態的類型。如果發生這種情況,您可以透過使用 as 轉換初始狀態,而不是宣告變數的類型來解決它

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

在元件中使用輸入掛勾

在元件檔中,匯入預先輸入的掛勾,而不是 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 設定檔作為範例

"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 邏輯

類型檢查 Reducer

Reducer 是純函式,接收目前的 state 和輸入的 action 作為參數,並回傳新的 state。

如果你使用 Redux Toolkit 的 createSlice,你很少需要另外明確輸入 reducer。如果你真的撰寫獨立的 reducer,通常只要宣告 initialState 值的類型,並將 action 的類型指定為 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> 類型,你也可以使用它。

類型檢查 Middleware

Middleware 是 Redux store 的擴充機制。Middleware 組成一個串聯,包裝 store 的 dispatch 方法,並存取 store 的 dispatchgetState 方法。

Redux 核心匯出 Middleware 類型,可用於正確輸入 middleware 函式

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 應使用 Middleware 類型,並在需要時傳遞 S (state) 和 D (dispatch) 的泛型引數

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 規則可能會在你使用 {} 作為 dispatch 值時回報錯誤。它建議的變更不正確,會中斷你的 Redux store 類型,你應該停用這行的規則,並繼續使用 {}

如果你在中間件中調用其他 thunk,則可能只需要調用通用。

在使用 type RootState = ReturnType<typeof store.getState> 的情況下,可以透過將 RootState 的類型定義切換為,來避免中間件和儲存定義之間的循環類型參考

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

使用 Redux Toolkit 切換 RootState 的類型定義範例

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

Redux Thunk 是用於撰寫與 Redux 儲存互動的同步和非同步邏輯的標準中間件。thunk 函式接收 dispatchgetState 作為其參數。Redux Thunk 有內建的 ThunkAction 類型,我們可以使用它來定義這些參數的類型

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 不允許只提供部分通用參數,因此其他參數的通常值為 EunknownAUnknownAction

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 時使用該類型

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

請注意,這假設 thunk 沒有有意義的回傳值。如果你的 thunk 回傳一個承諾,而你想要在調用 thunk 後使用回傳的承諾,你會想要將其用作 AppThunk<Promise<SomeReturnType>>

注意

別忘了預設的 useDispatch 勾子不知道 thunk,因此調用 thunk 會導致類型錯誤。務必在你的元件中使用更新形式的 Dispatch,它將 thunk 識別為可接受的調用類型

與 React Redux 一起使用

雖然 React Redux 是與 Redux 本身分開的函式庫,但它通常與 React 一起使用。

有關如何正確將 React Redux 與 TypeScript 一起使用的完整指南,請參閱React Redux 文件中的「靜態類型」頁面。本節將重點介紹標準模式。

如果你使用 TypeScript,React Redux 類型會在 DefinitelyTyped 中分開維護,但包含在 react-redux 套件的依賴項中,因此它們應該會自動安裝。如果你仍然需要手動安裝它們,請執行

npm install @types/react-redux

輸入 useSelector 勾子

在選擇器函式中宣告 state 參數的類型,useSelector 的回傳類型將推斷為與選擇器回傳類型相符

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)

這也可以內嵌完成

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

不過,建議建立預先輸入的 useAppSelector 掛鉤,並內建正確的 state 類型。

輸入 useDispatch 掛鉤

預設情況下,useDispatch 的傳回值是 Redux 核心類型定義的標準 Dispatch 類型,因此不需要宣告

const dispatch = useDispatch()

不過,建議建立預先輸入的 useAppDispatch 掛鉤,並內建正確的 Dispatch 類型。

輸入 connect 高階元件

如果您仍在使用 connect,您應該使用 @types/react-redux^7.1.2 匯出的 ConnectedProps<T> 類型,以自動推斷 connect 中的屬性類型。這需要將 connect(mapState, mapDispatch)(MyComponent) 呼叫拆分為兩部分

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 Toolkit 搭配使用

使用 TypeScript 的 標準 Redux Toolkit 專案設定 區段已經涵蓋了 configureStorecreateSlice 的一般使用模式,而 Redux Toolkit「與 TypeScript 搭配使用」頁面 則詳細介紹了所有 RTK API。

以下是使用 RTK 時常見的一些其他輸入模式。

輸入 configureStore

configureStore 從提供的根部 reducer 函式推斷 state 值的類型,因此不需要特定的類型宣告。

如果您要將其他中間件新增到儲存體,請務必使用 getDefaultMiddleware() 傳回的陣列中包含的特殊 .concat().prepend() 方法,因為這些方法會正確保留您要新增的中間件類型。(使用純粹的 JS 陣列散佈通常會遺失這些類型。)

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

比對動作

RTK 生成的動作建立器有一個 match 方法,它作為 類型謂詞。呼叫 someActionCreator.match(action) 會針對 action.type 字串進行字串比對,如果用作條件,則會將 action 的類型縮小為正確的 TS 類型

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

輸入 createSlice

定義單獨的案例簡化器

如果你有太多案例簡化器,而且內聯定義會很混亂,或者你想要在切片中重複使用案例簡化器,你也可以在 createSlice 呼叫外部定義它們,並將它們的類型設為 CaseReducer

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

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

輸入 extraReducers

如果你在 createSlice 中新增 extraReducers 欄位,請務必使用「建構器回呼」形式,因為「純物件」形式無法正確推斷動作類型。將 RTK 生成的動作建立器傳遞給 builder.addCase() 將正確推斷 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 回呼

如果你想要在動作中新增 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 } }
}
}
}
})

修正已匯出的切片中的循環類型

最後,在少數情況下,你可能需要使用特定類型匯出切片簡化器,以解決循環類型相依性問題。這可能如下所示

export default counterSlice.reducer as Reducer<Counter>

輸入 createAsyncThunk

對於基本用法,你只需要為 createAsyncThunk 提供單一引數的類型,作為你的 payload 建立回呼。你還應該確保回呼的傳回值類型正確

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 類型,你必須提供傳回類型和 payload 引數的前兩個泛型引數,以及物件中任何相關的「thunkApi 引數欄位」

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

輸入 createEntityAdapter 只需要你指定實體類型作為單一泛型引數。這通常如下所示

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

其他建議

使用 React Redux Hooks API

我們建議使用 React Redux hooks API 作為預設方法。hooks API 與 TypeScript 搭配使用時簡單許多,因為 useSelector 是接受選擇器函式的簡單 hook,而且傳回類型可以從 state 引數的類型輕鬆推斷出來。

雖然 connect 仍然運作良好,而且可以輸入,但正確輸入的難度高很多。

避免動作類型聯合

我們特別建議不要嘗試建立動作類型的聯集,因為它沒有提供真正的優點,而且實際上會在某些方面誤導編譯器。請參閱 RTK 維護者 Lenz Weber 的文章 不要使用 Redux 動作類型建立聯集類型,了解為什麼這是一個問題。

此外,如果你正在使用 createSlice,你已經知道該區塊定義的所有動作都已正確處理。

資源

如需進一步資訊,請參閱這些其他資源