[Redux] Redux TypeScript 筆記
- Redux Toolkit TypeScript Quick Start @ Redux > Tutorials > TypeScript Quick Start
- Usage with TypeScript @ Redux > Using Redux > Code Quality > Usage with TypeScript
- Redux Toolkit TypeScript Quick Start @ Redux Toolkit > Tutorials > TypeScript Quick Start
- Usage With TypeScript @ Redux Toolkit > Using Redux Toolkit > Usage with TypeScript
在 react-redux v7.2.3 後的版本,本來就會相依型別定義檔,開發者將可以不用自行下載型別定義。
TL;DR
Define Root State and Dispatch Types:
import { Action, AnyAction, ThunkAction } from '@reduxjs/toolkit';
import { Epic } from 'redux-observable';
import { store } from '.';
import rootReducer from './reducers';
// define types for redux
// https://redux.js.org/usage/usage-with-typescript#define-root-state-and-dispatch-types
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
// define types for redux-observable
export type AppState = ReturnType<typeof rootReducer>;
export type AppEpic = Epic<AnyAction, AnyAction, AppState>;
// define types for redux-thunk
// https://redux.js.org/usage/usage-with-typescript#type-checking-redux-thunks
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
Define Typed Hooks:
// src/store/utils.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import { AppDispatch, RootState } from './types';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Project Setup
Define Root State and Dispatch Types
// app/store.ts
export const store = configureStore({
/* ... */
});
// define types for redux
// https://redux.js.org/usage/usage-with-typescript#define-root-state-and-dispatch-types
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
建立帶有型別定義的 useDispatch 和 useSelector(Define Typed Hooks)
雖然可以在每個 Component 中直接使用定義好的 RootState
和 AppDispatch
,但如果能建立帶有型別資訊的 useDispatch
和 useSelector
會更好,如此就:
- 不用每次在元件中使用
useSelector
時還要額外寫(state: RootState)
- 不用每次在元件中使用
useDispatch
時,還要載入AppDispatch
來讓 useDispatch 知道 middleware 的型別資訊
// app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store';
// 之後在元件中,使用帶有型別資訊的 useDispatch 和 useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
後續在元件中需要使用原本的 useDispatch
和 useSelector
時,則都從 app/hook.ts
使用包過的 useAppDispatch
和 useAppSelector
。
ConfigureStore
Typing configureStore @ redux > Using Redux > Code Quality > Usage with TypeScript
在 configureStore
中,我們經常會添加額外的 middleware,建議使用 .concat()
和 .prepend()
方法來擴充需要的 middleware,而非 array spread,因為使用 array spread 有可能會遺失部分型別資訊:
// 透過 configureStore() 建立 Redux Store
export const store = configureStore({
reducer: rootReducer,
// 建議使用 concat 和 prepend,使用 array spread 可能會遺失型別資料
// https://redux.js.org/usage/usage-with-typescript#typing-configurestore
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(pokemonApi.middleware, epicMiddleware),
});
Application Usage
Define Slice State and Action Types
createSlice
中 state 的型別會被 initialState 的型別所決定,所以記得為 initialState 定義型別資訊- 如果某個 action 是帶有 payload 的,可以使用
PayloadAction<T>
中的T
來定義action.payload
的型別
// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk, RootState } from '../../store/types';
/**
* 定義 initialState 的型別資訊,目的是讓 createSlice 知道該 slice 中 state 的型別資訊
**/
export interface CounterState {
value: number;
}
export const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: 'counter',
// counterSlice 中,其 state 的型別會被 initialState 的型別所決定
initialState,
reducers: {
increment: (state) => ({
value: state.value + 1,
}),
decrement: (state) => ({
value: state.value - 1,
}),
// 使用 PayloadAction 型別來定義 action.payload 能夠接受的型別
// 這裡的 action.payload 需要時 number
incrementByAmount: (state, action: PayloadAction<number>) => ({
value: state.value + action.payload,
}),
},
});
Typing Additional Redux Logic
- Type Checking Reducers:在沒有用
createSlice
的情況下如何搭配型別AnyAction
建立 reducer - Type Checking Middleware:說明如何使用
Middleware
型別來建立客制化的 middleware - Type Checking Thunks:說明如何使用
ThunkAction
型別來定義並使用型別AppThunk
定義 AppThunk
這個 Type Utility:
// src/store/types.ts
import { Action, ThunkAction } from '@reduxjs/toolkit';
import { store } from '.';
// define types for redux
// https://redux.js.org/usage/usage-with-typescript#define-root-state-and-dispatch-types
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
// define types for redux-thunk
// https://redux.js.org/usage/usage-with-typescript#type-checking-redux-thunks
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
使用 AppThunk
:
// src/features/counter/counterSlice.ts
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
setTimeout(() => dispatch(incrementByAmount(amount)), 1000);
}
};
Usage with Redux Toolkit
-
Typing
configureStore
:如何在configureStore
中添加帶有型別資訊的 middlware -
Matching Actions:透過
createAction
建立的 action creator,其match
方法有 type predicate 的效果 -
- 如果需要在
createSlice
外建立 reducers 的話,可以搭配使用型別CaseReducer
來定義該 reducer - 如果需要使用
extraReducers
的話,建議搭配builder.addCase()
使用,將能推導出正確的型別 - 如果需要在 action 中添加
meta
和error
屬性、或是客製化payload
,需要搭配prrepare
callback 使用 - 若要解決 circular type dependency problem 的問題,可以使用
as Reducer<>
:
export default counterSlice.reducer as Reducer<Counter>
- 如果需要在
-
Typing
createAsyncThunk
:如果需要修改thunkApi
的型別,需要透過 generics 的參數自行調整 -
Typing
createEntityAdapter
:只需在createEntityAdapter<T>
的 T 帶入 entity 的型別:// 在 createEntityAdapter 的 generics 中帶入泛型
const booksAdapter = createEntityAdapter<Book>({
selectId: book => book.bookId,
sortComparer: (a, b) => a.title.localeCompare(b.title)
})
const booksSlice = createSlice({
name: 'books',
// 在 initialState 中,會自動根據 booksAdapter.getInitialState()
// 來推導 slice 中 state 的型別
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
booksAdapter.setAll(state, action.payload.books)
}
}
})
其他
- Redux 官方強烈建議不要建立 unions of action types,