はじめに:なぜ「一括移行」ではなく「段階的移行」なのか
長年運用されてきたNext.js(JavaScript)プロジェクトにおいて、ある日突然「今日から全てTypeScriptにします!」と宣言し、数千ファイルを一気に書き換えるのは現実的ではありません。
- ビジネスの停滞: 機能開発を止める必要がある
- リグレッションリスク: 単純な書き換えでもバグを生む可能性がある
- モチベーション低下: 終わりの見えない単純作業はチームを疲弊させる
したがって、既存のビジネスロジックを守りながら、「走りながらエンジンを交換する」ような段階的な移行戦略が必須となります。本記事では、その具体的な手順と、チームへの共有方法(READMEへの記述)に焦点を当てます。
フェーズ1:開発環境の準備と「共存」の設定
まずは、JavaScriptファイル(.js/.jsx)をそのまま残しつつ、TypeScriptファイル(.ts/.tsx)を追加できる「共存環境」を作ります。
最小限のtsconfig.json設定
Next.jsの場合、プロジェクトルートに空の tsconfig.json を作成して npm run dev を叩くだけで自動生成されますが、段階的移行においては以下の設定が肝になります。
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, // 重要:JSファイルをコンパイル対象に含める
"skipLibCheck": true,
"strict": false, // 初期段階ではfalse推奨(徐々にtrueへ)
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], // JSも含める
"exclude": ["node_modules"]
}
特に allowJs: true は必須です。また、最初から strict: true にすると既存のJSコードに対して大量のエラーが出るため、最初は false から始め、移行が進んだ段階で true に切り替えるのが定石です。
フェーズ2:README.mdに記すべき「TS化の思想」とルール
環境が整っても、運用ルールが曖昧だと「このファイルはTSにすべき?JSのままでいい?」とメンバーが迷います。 プロジェクトの README.md に「TS化セクション」を設け、以下の思想を明文化しましょう。
思想:既存コードは触らない、新規コードはTS必須
以下のような「ボーイスカウト・ルール(来た時よりも美しく)」を適用範囲を限定して導入します。
【TS化の方針】
- 新規作成するファイルは、必ずTypeScript(
.ts/.tsx)で作成する。- 既存のJSファイルは、機能修正やバグ改修のタイミングでのみTS化を検討する。(無理に書き換えない)
- 型定義に時間をかけすぎない。動くことが最優先。
ルール:Any型の扱いと「TODO:」コメントの運用
移行期において、厳密な型定義が難しい場面は多々あります。そこで「逃げ道」を用意します。
【型定義のルール】
- どうしても型がわからない場合は
anyを使用しても良い。- ただし、その際は必ず
// TODO: TS移行完了後に型を修正というコメントを残すこと。npm run type-checkコマンドを用意し、CIで落ちない範囲で型チェックを通すこと。
フェーズ3:具体的な移行パターンの実践
実際のコード移行は、影響範囲が明確な部分から着手します。
コンポーネントのProps定義から始める
ReactコンポーネントのPropsに型がつくだけで、利用側の開発体験(DX)は劇的に向上します。
Before (Button.jsx)
JavaScript
export const Button = ({ onClick, text, variant }) => { return <button className={btn-${variant}} onClick={onClick}>{text}</button>; };
After (Button.tsx)
TypeScript
`type ButtonProps = { onClick: () => void; text: string; variant?: 'primary' | 'secondary' | 'danger'; // 文字列リテラル型で安全性を担保 };
export const Button = ({ onClick, text, variant = 'primary' }: ButtonProps) => { return <button className={btn-${variant}} onClick={onClick}>{text}</button>; };`
このように variant に型が付くだけで、IDEの補完が効くようになり、タイポによるバグを防げます。
APIレスポンスの型定義による安全性確保
フロントエンドで最もバグりやすい「APIレスポンスのデータ構造」に型を定義します。これだけで「データが来ると思ったらundefinedだった」という事故を減らせます。
TypeScript
// types/api/user.ts export type User = { id: string; name: string; email?: string; // 任意項目の可視化 role: 'admin' | 'member'; };
まとめ:完了定義を決め、焦らず進める
「すべてのファイルを .ts にする」ことをゴールにすると、プロジェクトは疲弊します。 まずは「新規開発が100% TypeScriptで行われている状態」を第一のゴールとし、既存コードの移行は長期的な負債返済計画として進めることをお勧めします。
段階的な移行は、チームの技術力向上とプロダクトの安定性を両立する最良の手段です。今日から tsconfig.json を作成し、小さく始めてみませんか?

