移行ガイド
既存の JavaScript React プロジェクトを TypeScript に移行する方法を学びます
このチャプターで学ぶこと
- 段階的な移行戦略を理解できる
- tsconfig.json の移行用設定を使える
- JSX から TSX への変換手順を実践できる
- 型定義がないライブラリへの対処法を知る
移行ガイド
TypeScript コースの最終チャプターです。ここでは、既存の JavaScript React プロジェクトを TypeScript に段階的に移行する方法を学びます。「全部一気に書き換えなきゃ」と思う必要はありません。JavaScript と TypeScript は同じプロジェクト内で共存できるため、少しずつ安全に移行できます。
段階的な移行戦略
一括移行 vs 段階的移行
TypeScript への移行には 2 つのアプローチがあります。
| アプローチ | メリット | デメリット |
|---|---|---|
| 一括移行 | 一度で完了する | 巨大な PR になり、レビューが困難 |
| 段階的移行 | リスクが小さく、通常業務と並行可能 | 完了まで時間がかかる |
ほとんどのチームでは段階的移行を選びます。実務では新機能の開発やバグ修正と並行して進める必要があるため、一括移行は現実的ではありません。
Rails 6 から Rails 7 にアップグレードするとき、一気にすべてを変更するのではなく、deprecation warning を潰しながら段階的に進めますよね。TypeScript への移行も同じ考え方です。
移行の全体像
移行は大きく 4 つのフェーズで進めます。
フェーズ 1: 環境準備
TypeScript をインストールし、tsconfig.json を設定する
フェーズ 2: ゆるい設定で始める
strict: false で .jsx を .tsx にリネームしていく
フェーズ 3: 型を追加する
コンポーネントの Props・State に型を付けていく
フェーズ 4: strict モードを有効にする
strict: true にして残りの型エラーを解消する
フェーズ 1: 環境準備
TypeScript のインストール
既存の React プロジェクトに TypeScript を追加します。
# TypeScript と React の型定義をインストール
npm install --save-dev typescript @types/react @types/react-dom
tsconfig.json の作成
移行用の ゆるい設定 で始めます。
{
"compilerOptions": {
// 基本設定
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
// 移行用のゆるい設定(後で strict: true にする)
"strict": false,
"noImplicitAny": false,
// JavaScript ファイルも許可する(混在させるため)
"allowJs": true,
// 出力設定
"outDir": "./dist",
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}
allowJs: true—.jsと.tsファイルを混在できる。これが段階的移行の鍵ですstrict: false— 厳密な型チェックを無効にする。最初はゆるく始めて、移行が進んだら有効にしますnoImplicitAny: false— 型推論できない場合に暗黙的にanyを許可する。最初は許容して、後で潰していきますskipLibCheck: true— node_modules 内の型エラーをスキップ。移行中のノイズを減らします
Vite プロジェクトの場合
React コースで作ったプロジェクト(Vite ベース)の場合、Vite は TypeScript をネイティブでサポートしているため追加設定は最小限です。
# Vite プロジェクトでは TypeScript プラグインの追加は不要
# vite.config.js も .ts にリネームできるが、後回しでもOK
フェーズ 2: ファイルのリネーム
.jsx を .tsx にリネームする順序
リネームの順番が重要です。依存関係のツリーの末端(リーフコンポーネント)から始めます。
推奨する移行順序:
1. 共有の型定義ファイルを作る(src/types/index.ts)
2. ユーティリティ関数(.js → .ts)
3. リーフコンポーネント(他のコンポーネントに依存しないもの)
4. 中間コンポーネント
5. ページコンポーネント
6. App.tsx(ルートコンポーネント)
7. main.tsx(エントリーポイント)
なぜリーフから始めるかというと、他のファイルへの影響が最小限だからです。ルートから始めると、すべての子コンポーネントに型の不整合が波及します。
Blade コンポーネントを Livewire に移行するとき、末端のパーツコンポーネント(ボタン、インプットなど)から始めるのと同じ発想です。レイアウトテンプレートから手を付けると影響範囲が大きくなりすぎます。
リネームの具体的な手順
ファイルを 1 つずつ移行していきます。
# ステップ 1: ファイルをリネーム
mv src/components/Button.jsx src/components/Button.tsx
# ステップ 2: エディタで開いてエラーを確認
# ステップ 3: エラーを修正
# ステップ 4: 動作確認(ブラウザで表示が壊れていないか)
# ステップ 5: コミット
git add src/components/Button.tsx
git commit -m "chore: Button.jsx を TypeScript に移行"
1 ファイルの移行ごとにコミットすることを強く推奨します。問題が起きたときにすぐに元に戻せます。「10 ファイルまとめてリネーム」はリスクが高いです。
フェーズ 3: 型を追加する
コンポーネントに型を付ける(Before / After)
React コースで作ったコンポーネントを実際に TypeScript 化してみましょう。
Before(JavaScript):
// src/components/Profile.jsx
function Profile({ name, bio, imageUrl, hobbies = [] }) {
return (
<section>
{imageUrl && (
<img src={imageUrl} alt={`${name}のプロフィール写真`} />
)}
<div>
<h2>{name}</h2>
<p>{bio}</p>
{hobbies.length > 0 && (
<ul>
{hobbies.map((hobby) => (
<li key={hobby}>{hobby}</li>
))}
</ul>
)}
</div>
</section>
);
}
export default Profile;
After(TypeScript):
// src/components/Profile.tsx
// まず Props の型を定義する
type ProfileProps = {
name: string;
bio: string;
imageUrl?: string; // 省略可能なので ?
hobbies?: string[]; // 省略可能、デフォルト値あり
};
function Profile({ name, bio, imageUrl, hobbies = [] }: ProfileProps) {
return (
<section>
{imageUrl && (
<img src={imageUrl} alt={`${name}のプロフィール写真`} />
)}
<div>
<h2>{name}</h2>
<p>{bio}</p>
{hobbies.length > 0 && (
<ul>
{hobbies.map((hobby) => (
<li key={hobby}>{hobby}</li>
))}
</ul>
)}
</div>
</section>
);
}
export default Profile;
変更点は 2 つだけです。
ProfileProps型を定義した- 関数の引数に
: ProfilePropsを追加した
コンポーネントの中身は一切変わっていません。これが TypeScript 移行の基本です。
State を持つコンポーネントの移行
Before(JavaScript):
// src/components/Counter.jsx
import { useState } from 'react';
function Counter({ initialCount = 0, step = 1, onCountChange }) {
const [count, setCount] = useState(initialCount);
const increment = () => {
const newCount = count + step;
setCount(newCount);
onCountChange?.(newCount);
};
const decrement = () => {
const newCount = count - step;
setCount(newCount);
onCountChange?.(newCount);
};
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
export default Counter;
After(TypeScript):
// src/components/Counter.tsx
import { useState } from "react";
type CounterProps = {
initialCount?: number;
step?: number;
onCountChange?: (count: number) => void; // コールバック関数の型
};
function Counter({
initialCount = 0,
step = 1,
onCountChange,
}: CounterProps) {
// useState の型は initialCount の型から自動推論される
const [count, setCount] = useState(initialCount);
const increment = () => {
const newCount = count + step;
setCount(newCount);
onCountChange?.(newCount);
};
const decrement = () => {
const newCount = count - step;
setCount(newCount);
onCountChange?.(newCount);
};
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
export default Counter;
useState の型引数はほとんどの場合省略可能です。初期値から自動推論されるためです。useState(0) なら number、useState("") なら string と推論されます。明示的に指定が必要なのは useState<User | null>(null) のように初期値が null の場合です。
共通の型定義ファイルを作る
プロジェクト全体で使う型は、専用のファイルにまとめましょう。
// src/types/index.ts
// ユーザー関連の型
export type User = {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
};
// API レスポンスの共通型
export type ApiResponse<T> = {
data: T;
status: "success" | "error";
message?: string;
};
// フォーム関連の型
export type ContactFormData = {
name: string;
email: string;
message: string;
};
// 使う側
import type { User } from "../types";
type UserCardProps = {
user: User;
onEdit: (id: number) => void;
};
any との付き合い方
移行中の any は「技術的負債のマーカー」
移行中はどうしても any を使う場面が出てきます。大切なのは、any を意図的に、一時的に使うことです。
// 悪い例:何も考えずに any を使う
const data: any = fetchSomething();
// 良い例:TODO コメント付きで any を使う
// TODO: API レスポンスの型を定義する (#123)
const data: any = fetchSomething();
プロジェクト内の any の数を定期的に確認する習慣をつけましょう。
# プロジェクト内の any の使用箇所を検索
grep -rn ": any" src/ --include="*.tsx" --include="*.ts"移行の進捗指標として「any の残り数」を使うと、ゴールが見えやすくなります。
any を段階的に排除する
any を潰すときは、以下の優先順位で進めます。
// ステップ 1: any → unknown(型安全だが使いにくい)
const data: unknown = fetchSomething();
// ステップ 2: unknown → 型ガード付き
function isUser(data: unknown): data is User {
return (
typeof data === "object" &&
data !== null &&
"id" in data &&
"name" in data
);
}
if (isUser(data)) {
console.log(data.name); // 安全にアクセスできる
}
// ステップ 3: 最初から正しい型を返す関数に書き換える
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: User = await response.json();
return data;
}
型定義がないライブラリへの対処法
3 つの対処パターン
サードパーティライブラリの型定義がない場合、段階的に対処します。
// パターン 1: @types パッケージを探す(最優先)
// npm install --save-dev @types/ライブラリ名
// パターン 2: 最低限の型宣言を書く
// src/types/legacy-chart-lib.d.ts
declare module "legacy-chart-lib" {
// 使っている関数だけ型を定義する
export function createChart(
element: HTMLElement,
options: {
type: "bar" | "line" | "pie";
data: number[];
labels: string[];
}
): void;
export function updateChart(data: number[]): void;
}
// パターン 3: any で宣言する(最終手段)
// src/types/very-old-lib.d.ts
declare module "very-old-lib";
// これだけで import できるようになる(全て any 扱い)
パターン 3 は型安全性がゼロになるため、本当に他に方法がない場合のみ使いましょう。移行初期のブロッカー解消用と割り切り、後から型を追加するタスクを忘れずに作成してください。
フェーズ 4: strict モードの有効化
strict: true に切り替える
移行がある程度進んだら、tsconfig.json の設定を厳しくしていきます。
{
"compilerOptions": {
// 一気に strict: true にするのではなく、個別に有効化していく
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
// 上記をすべて有効にしたら、まとめて strict: true に置き換える
// "strict": true
}
}
strict: true は上記すべてのオプションをまとめて有効にするショートカットです。一気に有効にすると大量のエラーが出る可能性があるため、1 つずつ有効にしてエラーを潰していく方が安全です。特に影響が大きいのは noImplicitAny と strictNullChecks です。
strictNullChecks で見つかる典型的なエラー
strictNullChecks を有効にすると、null や undefined の可能性がある箇所がすべてエラーになります。
// strictNullChecks: true でエラーになる例
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User>(); // User | undefined
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// エラー!user が undefined の可能性がある
return <h1>{user.name}</h1>;
// 修正方法 1: 条件付きレンダリング
if (!user) return <p>読み込み中...</p>;
return <h1>{user.name}</h1>;
// 修正方法 2: オプショナルチェーン
return <h1>{user?.name ?? "読み込み中..."}</h1>;
}
これは TypeScript の大きな恩恵です。「null チェックし忘れ」というバグをコンパイル時に防止できます。
ベストプラクティスまとめ
移行を成功させるための心得をまとめます。
1. 完璧を求めない
“Perfect is the enemy of good” — 完璧は善の敵
最初から完璧な型を付ける必要はありません。any を使ってでもまず移行を進め、後から改善する方が生産的です。
2. 新規ファイルは最初から TypeScript で
移行中でも、新しく作るファイルは .tsx / .ts で作りましょう。JavaScript のファイルを増やすのは後退です。
3. チーム全体で合意する
移行はチーム全体のタスクです。以下のルールをチームで決めておきましょう。
- 新規ファイルは TypeScript 必須
- 既存ファイルを修正するときは、ついでに
.tsxに移行する anyを使うときは TODO コメントを付ける- 週に N ファイルのペースで移行する
4. CI で型チェックを回す
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc --noEmit && vite build"
}
}
tsc --noEmit を CI に組み込むことで、型エラーのあるコードがマージされるのを防げます。
React コースのチャプター 4 で作った「カウンター」アプリを TypeScript に移行してみましょう。
移行対象のコード(JavaScript):
// src/App.jsx
import { useState } from 'react';
function Counter({ label, initialCount, step }) {
const [count, setCount] = useState(initialCount);
return (
<div style={{ textAlign: 'center', padding: '1rem' }}>
<h2>{label}</h2>
<p style={{ fontSize: '2rem' }}>{count}</p>
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center' }}>
<button onClick={() => setCount(prev => prev - step)}>
-{step}
</button>
<button onClick={() => setCount(0)}>
リセット
</button>
<button onClick={() => setCount(prev => prev + step)}>
+{step}
</button>
</div>
</div>
);
}
function App() {
const [total, setTotal] = useState(0);
return (
<div>
<h1>カウンターアプリ</h1>
<Counter label="カウンターA" initialCount={0} step={1} />
<Counter label="カウンターB" initialCount={10} step={5} />
</div>
);
}
export default App;やること:
- ファイルを
App.tsxにリネームする(実際にはコピーして書き直す) Counterコンポーネントの Props 型(CounterProps)を定義する- 各 Props の型を決める:
label: 文字列initialCount: 数値(省略時は0)step: 数値(省略時は1)
useStateの型が正しく推論されているか確認する- エディタで赤い波線(型エラー)が出ないことを確認する
模範解答:
// src/App.tsx
import { useState } from "react";
// Props の型を定義
type CounterProps = {
label: string;
initialCount?: number;
step?: number;
};
function Counter({ label, initialCount = 0, step = 1 }: CounterProps) {
// useState の型は initialCount(number)から自動推論される
const [count, setCount] = useState(initialCount);
return (
<div style={{ textAlign: "center", padding: "1rem" }}>
<h2>{label}</h2>
<p style={{ fontSize: "2rem" }}>{count}</p>
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "center" }}>
<button onClick={() => setCount((prev) => prev - step)}>
-{step}
</button>
<button onClick={() => setCount(0)}>リセット</button>
<button onClick={() => setCount((prev) => prev + step)}>
+{step}
</button>
</div>
</div>
);
}
function App() {
return (
<div>
<h1>カウンターアプリ</h1>
<Counter label="カウンターA" />
<Counter label="カウンターB" initialCount={10} step={5} />
{/* 以下はエラーになることを確認しよう */}
{/* <Counter /> */}
{/* label が必須なのでエラー */}
{/* <Counter label="C" step="3" /> */}
{/* step は number なのに string を渡しているのでエラー */}
</div>
);
}
export default App;チャレンジ:
onCountChange?: (count: number) => voidを Props に追加して、カウントが変わったときに親コンポーネントに通知する仕組みを作ってみようCounterを別ファイル(src/components/Counter.tsx)に切り出して、import/exportの TypeScript 版を体験してみよう
コース全体のまとめと次のステップ
TypeScript コースお疲れさまでした! 全 10 チャプターを通して、以下の内容を学びました。
| チャプター | 学んだこと |
|---|---|
| 0. 環境構築 | TypeScript + React の開発環境セットアップ |
| 1. 基本の型 | string, number, boolean, 配列, オブジェクト |
| 2. 型注釈と型推論 | 明示的な型注釈と TypeScript の自動推論 |
| 3. インターフェースと型エイリアス | interface と type でオブジェクトの形を定義 |
| 4. ユニオン型とリテラル型 | 複数の型を組み合わせる | と型の絞り込み |
| 5. 関数の型 | パラメータ・戻り値・コールバックの型定義 |
| 6. ジェネリクス | 型をパラメータ化して汎用的なコードを作成 |
| 7. React + TS 基礎 | Props・State・イベントに型を付ける |
| 8. React + TS Hooks | useRef・useContext・カスタムフックの型 |
| 9. 実践パターン | as const・never・判別付きユニオン |
| 10. 移行ガイド | 既存プロジェクトの段階的な TypeScript 化 |
次のステップ
TypeScript の学習をさらに深めるためのリソースを紹介します。
公式ドキュメント:
- TypeScript Handbook — TypeScript の公式ガイド。困ったときの辞書として使えます
- TypeScript Playground — ブラウザ上で TypeScript を試せる環境。型の挙動を確認するのに便利です
React + TypeScript 特化:
- React TypeScript Cheatsheet — React で TypeScript を使う際の定番チートシート。Props、Hooks、Context などのパターンが網羅されています
- Matt Pocock の Total TypeScript — TypeScript の実践的なテクニックを学べるコース
実践:
- React コースで作ったすべてのコンポーネントを TypeScript に移行してみましょう
- 新しいプロジェクトを始めるときは、最初から TypeScript を使いましょう(
npm create vite@latest my-app -- --template react-ts) - 業務で使っている JavaScript プロジェクトに、まず
tsconfig.jsonと@typesパッケージだけ追加してみましょう
TypeScript は書いて覚えるのが一番です。公式ドキュメントを読むだけでなく、実際のプロジェクトで使いながら、エラーメッセージと向き合い、型の書き方を体に染み込ませていきましょう。最初は面倒に感じますが、1-2 週間もすれば「型がないと不安」と感じるようになります。
TypeScript は、バックエンドでいえば PHPStan や RuboCop のような静的解析をさらに強力にしたものです。コードを書く段階でバグを防ぎ、リファクタリングを安全にし、チーム開発を円滑にしてくれます。ぜひ今日から、あなたのプロジェクトに TypeScript を取り入れてみてください。