React での開発に慣れてくると、次に挑戦したくなるのがTypeScriptの導入ではないでしょうか。TypeScript は、JavaScript に静的な型定義を加えることで、コードの品質と開発者体験(DX)を劇的に向上させます。

  • バグの早期発見: 型の不整合をコーディング中に検知し、実行時エラーを未然に防ぎます。
  • 優れたエディタサポート: 関数の引数やオブジェクトのプロパティを自動補完し、開発効率を高めます。
  • メンテナンス性の向上: コードの意図が明確になり、大規模なプロジェクトやチーム開発での保守が容易になります。

この記事では、JavaScript ベースの React/Redux Toolkit で構築したシンプルなカウンターを、安全かつ段階的に TypeScript へリファクタリングする実践的な手順を解説します。

TypeScript の導入

まずは、プロジェクトに TypeScript とその型定義ファイルを追加し、設定ファイルを作成します。

1. 必要なパッケージのインストール

以下のコマンドで、TypeScript 本体と、React や Node.js の型定義ライブラリをインストールします。

npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest

2. TypeScript 設定ファイル(tsconfig.json)の作成

次に、プロジェクトのルートで以下のコマンドを実行し、TypeScript の設定ファイルtsconfig.jsonを生成します。

npx tsc --init

生成されたtsconfig.jsonを開き、React プロジェクト向けに最低限必要な設定を有効化します。特に"strict": trueは、TypeScript の強力な型チェック機能を最大限に活用するために不可欠です。

tsconfig.json の設定例

{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}

"allowJs": trueを設定することで、既存の.jsファイルと新しい.tsファイルを共存させながら、段階的なリファクタリングが可能になります。

React コンポーネントの TypeScript 変換

設定が完了したら、コンポーネントファイルを.jsxから.tsxへリネームし、型定義を追加していきます。

App.jsxからApp.tsx

このコンポーネントは props を受け取らないため、ファイル拡張子を.tsxに変更するだけで基本的には完了です。

src/App.tsx

import React from "react";
import { Counter } from "./features/counter/Counter";
import "./App.css";

function App() {
return (
<div><header></header></div>
);
}

export default App;

Counter.jsxからCounter.tsx

こちらも props がないため、拡張子を.tsxに変更するだけで TypeScript 化は完了です。後の手順で Redux のフックに型を適用することで、このコンポーネントの型安全性はさらに向上します。

src/features/counter/Counter.tsx

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { decrement, increment } from "./counterSlice";

export function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();

return (
<div>
<div><button aria-label="Increment value"> dispatch(increment())}>
+
</button>
{count}
<button aria-label="Decrement value"> dispatch(decrement())}>
-
</button></div>
</div>
);
}

Redux 関連ファイルの TypeScript 対応

ここがリファクタリングの核心部です。ストア、Slice、そしてそれらを利用するフックに型を定義していきます。

1. ストアからRootStateAppDispatchの型を定義する

まず、ストアの型情報を抽出し、アプリケーション全体で再利用できるようにします。store.jsstore.tsにリネームして編集します。

src/app/store.ts

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";

export const store = configureStore({
reducer: {
counter: counterReducer,
},
});

// ストアの型情報から`RootState`と`AppDispatch`の型を推論しエクスポート
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;

2. 型付けされたカスタムフックを作成する

毎回useSelectoruseDispatchで型をインポートするのは手間なので、あらかじめ型情報を付与したカスタムフックを作成します。これは公式で推奨されているベストプラクティスです。

src/app/hooks.ts

import { useDispatch, useSelector } from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch } from "./store";

// 型付けされたuseDispatchとuseSelectorをエクスポートし、通常のものの代わりに使用する
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook = useSelector;

3. Slice に型を適用する

createSliceにも型を適用します。counterSlice.jscounterSlice.tsにリネームし、initialStateの型を定義します。

src/features/counter/counterSlice.ts

import { createSlice } from "@reduxjs/toolkit";

// このSliceで管理するStateの型を定義
interface CounterState {
value: number;
}

// 型を指定してinitialStateを作成
const initialState: CounterState = {
value: 0,
};

export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

4. コンポーネントでカスタムフックを使用する

最後に、Counter.tsxで標準のフックの代わりに、先ほど作成した型付きカスタムフックを使用します。

src/features/counter/Counter.tsx (修正後)

import React from "react";
// React-Reduxのフックの代わりに、作成したカスタムフックをインポート
import { useAppSelector, useAppDispatch } from "../../app/hooks";
import { decrement, increment } from "./counterSlice";

export function Counter() {
// `useAppSelector`を使えば、stateは自動的に`RootState`型になり、エディタの補完が効く
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();

return (
<div>
<div><button aria-label="Increment value"> dispatch(increment())}>
+
</button>
{count}
<button aria-label="Decrement value"> dispatch(decrement())}>
-
</button></div>
</div>
);
}

これで、statedispatchが完全に型付けされた状態で利用でき、state.counterの存在やその中のvaluenumber型であることなどが保証されます。

最終チェックとまとめ

以上が、既存の React/Redux プロジェクトを TypeScript へリファクタリングする基本的な流れです。

  • TypeScript と型定義をインストールし、tsconfig.jsonを設定する。
  • コンポーネントを.tsxにリネームし、必要に応じて Props やイベントに型を定義する。
  • Redux ストアから型を抽出し、型付けされたカスタムフックを作成する。
  • Slice のinitialStateに型を定義し、コンポーネントでカスタムフックを使用する。

本記事で紹介した内容は、技術的な正確性を担保し、公式ドキュメントで推奨されているベストプラクティスに沿っています。段階的にリファクタリングを進めることで、安全にプロジェクトの品質を向上させることができます。ぜひ、TypeScript への第一歩を踏み出してみてください。