このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
TypeScriptでの使用
- TypeScriptを使ったReduxアプリケーションの標準的な設定パターン
- Reduxロジックの各部分を正しく型付けするテクニック
- TypeScriptの構文と用語の理解
- ジェネリクスやユーティリティ型などのTypeScriptコンセプトの知識
- React Hooksの理解
概要
TypeScriptはJavaScriptの型付きスーパーセットであり、ソースコードのコンパイル時チェックを提供します。Reduxと組み合わせて使用すると、TypeScriptは以下を実現するのに役立ちます:
-
リデューサー、ステート、アクションクリエーター、UIコンポーネントの型安全性
-
型付きコードの容易なリファクタリング
-
チーム環境における優れた開発者体験
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 型定義パッケージにあります。ライブラリ関数の型付けに加え、これらの型はReduxストアとReactコンポーネント間の型安全なインターフェースを容易に記述できるよう支援するヘルパーもエクスポートします。
React Redux v7.2.3以降では、react-reduxパッケージが@types/react-reduxに依存しているため、型定義はライブラリと共に自動的にインストールされます。それ以外の場合は、手動でインストールする必要があります(通常はnpm install @types/react-redux)。
Create-React-App向けRedux+TSテンプレートには、これらのパターンが設定済みの動作サンプルが含まれています。
Root StateとDispatch型の定義
configureStoreの使用には追加の型付けは不要です。ただし、必要に応じて参照できるようRootState型とDispatch型を抽出することをお勧めします。これらの型をストア自体から推論することで、ステートスライスを追加したりミドルウェア設定を変更したりした際に正しく更新されます。
これらは型であるため、app/store.tsなどのストア設定ファイルから直接エクスポートし、他のファイルに直接インポートしても安全です。
import { configureStore } from '@reduxjs/toolkit'
// ...
export const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer
}
})
// Get the type of our store variable
export type AppStore = typeof store
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch']
型付きフックの定義
各コンポーネントにRootStateとAppDispatch型をインポートすることも可能ですが、アプリケーションで使用するために**useDispatchとuseSelectorフックの事前型付けバージョンを作成する**方が良いでしょう。これにはいくつかの重要な理由があります:
-
useSelectorの場合、毎回(state: RootState)と型指定する手間が省けます -
useDispatchの場合、デフォルトのDispatch型はthunkや他のミドルウェアを認識しません。thunkを正しくディスパッチするには、ストアからthunkミドルウェアの型を含むカスタマイズされたAppDispatch型を使用し、それをuseDispatchと組み合わせる必要があります。事前に型付けされたuseDispatchフックを追加することで、必要な場所でAppDispatchをインポートし忘れるのを防げます。
これらは型ではなく実際の変数であるため、ストア設定ファイルではなくapp/hooks.tsのような別ファイルで定義することが重要です。これにより、フックを使用する必要がある任意のコンポーネントファイルにインポートでき、循環インポートの依存関係問題を回避できます。
.withTypes()の使用
以前は、アプリ設定でフックを「事前に型付け」する方法にはいくつかのバリエーションがありました。結果は以下のスニペットのようになります:
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では、Redux ToolkitのcreateAsyncThunkにある.withTypesメソッドと同様に、これらの各フックに新しい.withTypesメソッドが追加されました。
これにより設定は次のようになります:
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コンパイラは型に関してこれを正しく処理できます。セレクター関数の作成などのユースケースで必要になる場合があります。
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
生成されたアクションクリエーターは、リデューサー用に指定したPayloadAction<T>型に基づいて、payload引数を受け入れるよう正しく型付けされます。例えば、incrementByAmountは引数としてnumberを要求します。
場合によってはTypeScriptが初期状態の型を不必要に厳密化することがあります。そのような場合、変数の型を宣言する代わりにasを使用して初期状態をキャストすることで回避できます:
// Workaround: cast state instead of declaring variable type
const initialState = {
value: 0
} as CounterState
コンポーネントでの型付きフックの使用
コンポーネントファイルでは、React Reduxの標準フックではなく、事前に型付けされたフックをインポートしてください。
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はチームが正しいフックを簡単にインポートするのに役立ちます。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ロジックの型付け
リデューサーの型チェック
リデューサーは純粋な関数で、現在のstateと受信したactionを引数として受け取り、新しいステートを返します。
Redux ToolkitのcreateSliceを使用している場合、リデューサーを個別に型付けする必要はほとんどありません。スタンドアロンのリデューサーを実際に記述する場合、通常は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>型もエクスポートしており、こちらも使用できます。
ミドルウェアの型チェック
ミドルウェアはReduxストアの拡張メカニズムです。ミドルウェアはストアのdispatchメソッドをラップするパイプラインに構成され、ストアのdispatchとgetStateメソッドにアクセスできます。
Reduxコアはミドルウェア関数を正しく型付けするために使用できる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型を使用し、必要に応じてS(ステート)とD(ディスパッチ)のジェネリック引数を渡すべきです:
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ルールがエラーを報告する可能性があります。推奨される修正は不正確であり、Reduxストアの型を壊す可能性があるため、この行ではルールを無効にして{}の使用を継続してください。
ディスパッチのジェネリックは、ミドルウェア内で追加の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 Thunkの型チェック
Redux Thunkは、Reduxストアと連携する同期/非同期ロジックを記述するための標準ミドルウェアです。Thunk関数はパラメータとしてdispatchとgetStateを受け取ります。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(ステート)を指定する必要があります。ただしTypeScriptでは一部のジェネリック引数のみを指定することができないため、他の引数には通常Eにはunknown、AにはUnknownActionを使用します:
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がPromiseを返し、thunkディスパッチ後に返されたPromiseを使用する場合、AppThunk<Promise<SomeReturnType>>のように指定する必要があります。
デフォルトのuseDispatchフックはthunkを認識しないため、thunkをディスパッチすると型エラーが発生します。必ずコンポーネント内でthunkのディスパッチを認識する更新されたDispatch形式を使用してください。
React Reduxでの使用方法
React ReduxはRedux本体とは別ライブラリですが、Reactと共によく使用されます。
TypeScriptでReact Reduxを正しく使用する完全なガイドについては、**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)
ただし、stateの正しい型が組み込まれた事前型付けされたuseAppSelectorフックを作成することを推奨します。
useDispatchフックの型付け
デフォルトでは、useDispatchの戻り値はReduxコア型で定義された標準のDispatch型であるため、宣言は不要です:
const dispatch = useDispatch()
ただし、正しいDispatch型が組み込まれた事前型付けされたuseAppDispatchフックを作成することを推奨します。
connect高階コンポーネントの型付け
まだconnectを使用している場合、@types/react-redux^7.1.2からエクスポートされるConnectedProps<T>型を使用して、connectからpropsの型を自動推論すべきです。これにはconnect(mapState, mapDispatch)(MyComponent)呼び出しを2段階に分割する必要があります:
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プロジェクトの設定セクションでは、configureStoreとcreateSliceの通常の使用パターンを既に説明しています。詳細についてはRedux Toolkit「TypeScriptでの使用」ページを参照してください。
以下に、RTKを使用する際によく見られる追加の型付けパターンを紹介します。
configureStoreの型付け
configureStoreは提供されたルートリデューサー関数からステート値の型を推論するため、特定の型宣言は通常不要です。
ストアに追加のミドルウェアを導入する場合は、getDefaultMiddleware()が返す配列に含まれる専用の.concat()および.prepend()メソッドを使用してください。これにより追加するミドルウェアの型情報が正しく保持されます(通常のJavaScriptの配列スプレッド構文では型情報が失われることがあります)。
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が生成するアクションクリエーターには型述語(type predicate)として機能するmatchメソッドがあります。someActionCreator.match(action)を呼び出すとaction.typeの文字列比較が行われ、条件式として使用した場合にactionの型を適切なTypeScript型に絞り込みます:
const increment = createAction<number>('increment')
function test(action: Action) {
if (increment.match(action)) {
// action.payload inferred correctly here
const num = 5 + action.payload
}
}
この機能は、カスタムミドルウェアやredux-observable、RxJSのfilterメソッドなど、Reduxミドルウェア内でアクションタイプをチェックする際に特に有用です。
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フィールドを追加する場合は、「プレーンオブジェクト」形式ではなく「ビルダーコールバック」形式を使用してください。「プレーンオブジェクト」形式ではアクションタイプを正しく推論できません。builder.addCase()にRTK生成のアクションクリエーターを渡すと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コールバックの型付け
アクションに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 } }
}
}
}
})
エクスポートするスライスの循環型問題の解決
まれなケースですが、循環型依存問題を解決するために特定の型を付けてスライスリデューサーをエクスポートする必要がある場合があります。実装例:
export default counterSlice.reducer as Reducer<Counter>
createAsyncThunkの型付け
基本的な使用法では、createAsyncThunkに提供する必要がある型はペイロード生成コールバックの単一引数の型だけです。コールバックの戻り値の型付けも適切に行ってください:
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))
getState()が返すstateの型など、thunkApiパラメーターの型を変更する必要がある場合は、戻り値の型とペイロード引数用に最初の2つのジェネリック引数に加え、関連する「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の型付け
TypeScriptでのcreateEntityAdapterの使用方法は、エンティティがidプロパティで正規化されているか、カスタムselectIdが必要かによって異なります。
エンティティがidプロパティで正規化されている場合、createEntityAdapterはジェネリック引数としてエンティティ型のみを指定すれば十分です。例:
interface Book {
id: number
title: string
}
// no selectId needed here, as the entity has an `id` property we can default to
const booksAdapter = createEntityAdapter<Book>({
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)
}
}
})
一方、異なるプロパティで正規化する必要がある場合は、カスタムselectId関数を渡してそこで型注釈を行うことを推奨します。これにより手動で提供せずともIDの型を適切に推論できます。
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フックAPIの使用
デフォルトのアプローチとしてReact ReduxのフックAPIを使用することを推奨します。フックAPIはTypeScriptとの相性が良く、useSelectorはセレクター関数を受け取る単純なフックであり、戻り値の型はstate引数の型から簡単に推論できます。
connectも動作しますが、型付けが可能ではあるものの、正しく型付けするのはより困難です。
アクションタイプのユニオン型の回避
特にアクションタイプのユニオン型作成は推奨しません。実際の利点がなく、むしろコンパイラを誤解させる場合があるためです。この問題の理由については、RTKメンテナーのLenz Weberの記事「Reduxアクションタイプでユニオン型を作成しないでください」を参照してください。
さらに、createSliceを使用している場合、そのスライスで定義されたすべてのアクションが正しく処理されていることは既にわかっています。
参考資料
詳細については、以下の追加資料を参照してください:
-
Reduxライブラリのドキュメント:
- React Redux ドキュメント: 静的型付け: TypeScriptでReact Redux APIを使用する方法の例
- Redux Toolkit ドキュメント: TypeScriptでの使用: TypeScriptでRedux Toolkit APIを使用する方法の例
-
React + Redux + TypeScript ガイド:
- React+TypeScript チートシート: TypeScriptとReactを使用するための包括的なガイド
- TypeScript ガイド: React + Redux: TypeScriptでReactとReduxを使用するパターンに関する豊富な情報
- 注: このガイドには有用な情報もありますが、アクションタイプのユニオン型の使用など、このページで推奨するプラクティスに反するパターンが多く含まれています。完全性のためにリンクしています
-
その他の記事: