TypeScript 速習チュートリアル

JavaScript から TypeScript への移行ガイド

JavaScript から TypeScript への移行は、コードベースの保守性と開発体験(DX)を劇的に向上させます。
本ガイドでは、そのプロセスを一歩ずつ順を追って解説していきます。

1. 準備フェーズ

1.1 コードベースの現状把握

移行を開始する前に、以下の点を確認してください:

  • コードベースの規模と複雑さを特定する
  • ビルドプロセスと依存関係をドキュメント化する
  • 既存の型定義ファイル(.d.ts ファイル)があるか確認する
  • 特別な注意が必要なクリティカルパス(重要な処理)を特定する

1.2 バージョン管理の設定

クリーンな Git リポジトリ(または同等のもの)があることを確認してください。

CLI コマンド

# 移行用の新しいブランチを作成
git checkout -b typescript-migration

# 現在の状態をコミット
git add .
git commit -m "Pre-TypeScript migration state"

2. コンフィギュレーション(設定)

2.1 TypeScript のインストール

TypeScript を開発依存関係(dev dependency)としてインストールします。

CLI コマンド

# TypeScript と Node.js 用の型定義をインストール
npm install --save-dev typescript @types/node

2.2 tsconfig.json の作成

まずは基本的な tsconfig.json を作成します。

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

       注意: サポートする最小限の環境に合わせて target を調整してください。

3. 移行のアプローチ

3.1 段階的な移行(Gradual Migration)

残りのファイルを JavaScript のまま保持しつつ、1ファイルずつ移行します。

tsconfig.json 設定例

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true
  }
}
  • 最適: 大規模なコードベース、開発の中断を最小限に抑えたい場合。

3.2 一括移行(All-at-Once Migration)

すべての .js ファイルを .ts にリネームし、発生したエラーをすべて修正します。

CLI コマンド

# すべての JS ファイルを TS にリネーム(Linux/macOS)
find src -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;
  • 最適: 小・中規模プロジェクト、または新規プロジェクトの初期段階。

       重要な注意点: 大規模プロジェクトでは、混乱を最小限に抑え、プロセスを管理しやすくするために段階的な移行アプローチを強く推奨します。

4. ステップ・バイ・ステップの移行プロセス

4.1 基本設定の開始

以下の推奨設定で、基本的な tsconfig.json を作成します。

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "allowJs": true,
    "checkJs": true,
    "noEmit": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

4.2 JavaScript での型チェックの有効化

JavaScript ファイルの先頭に // @ts-check を追加することで、型チェックを有効にできます。

コード例

// @ts-check

/** @type {string} */
const name = 'John';

// TypeScript がこのエラーをキャッチします
name = 42; // エラー: 型 '42' を型 'string' に割り当てることはできません

       注意: 特定の行の型チェックを無効にするには // @ts-ignore を使用します。

4.3 ファイル拡張子を .ts に変更

重要度の低いファイルから順に、拡張子を .js から .ts に変更していきます。

CLI コマンド

# 単一ファイルのリネーム
mv src/utils/helpers.js src/utils/helpers.ts

# またはディレクトリ内のすべてのファイルをリネーム(慎重に使用してください)
find src/utils -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;

4.4 型アノテーションの追加

コードに少しずつ型アノテーション(型定義)を加えていきます。

コード例

// 移行前
function add(a, b) {
  return a + b;
}

// 移行後
function add(a: number, b: number): number {
  return a + b;
}

// インターフェースの使用
interface User {
  id: number;
  name: string;
  email?: string;
}

function getUser(id: number): User {
  return { id, name: 'John Doe' };
}

4.5 ビルドおよびテストスクリプトの更新

package.json を修正し、TypeScript のコンパイルを含めるようにします。

package.json

{
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "test": "jest"
  }
}

       注意: テストフレームワークの設定も TypeScript ファイルを扱えるように更新してください。

5. 移行ツール

  • ts-migrate: JavaScript から TypeScript への移行を自動化するツール(Facebook製)。
    • npx ts-migrate-full .
  • TypeStat: 型の安全性を保ちつつ JavaScript を TypeScript に変換します。
    • npx typestat
  • @types パッケージ: 依存関係にあるライブラリの型定義をインストールします。
    • npm install --save-dev @types/react @types/node

6. TypeScript 移行のベストプラクティス

6.1 小さく始めて繰り返す

  • ユーティリティ関数や非 UI コンポーネントから始める
  • 一度に移行するのは1ファイルまたは1モジュールに絞る
  • 各移行ステップが成功するたびにコミットする

6.2 TypeScript 機能を活用する

コード例

// 可能な限り型推論(Type Inference)を活用する
const name = 'John'; // TypeScript は 'string' と推論します
const age = 30;     // TypeScript は 'number' と推論します

// 柔軟性のためにユニオン型(Union Types)を使用する
type Status = 'active' | 'inactive' | 'pending';

// ランタイムチェックのために型ガード(Type Guards)を使用する
function isString(value: any): value is string {
  return typeof value === 'string';
}

6.3 サードパーティライブラリの処理

  • 依存関係に対して @types パッケージをインストールする
  • 型定義がないライブラリには宣言ファイル(.d.ts)を作成する
  • グローバルな型の拡張には declare module を使用する

7. よくある課題と解決策

7.1 動的なプロパティ

課題: JavaScript ではオブジェクトをディクショナリとしてよく使用しますが、TS ではエラーになることがあります。

コード例

// 移行前
const user = {};
user.name = 'John'; // エラー: プロパティ 'name' は存在しません

解決策: インデックスシグネチャ(Index Signature)または型アサーションを使用します。

コード例

// オプション 1: インデックスシグネチャ
interface User {
  [key: string]: any;
}
const user: User = {};
user.name = 'John'; // OK

// オプション 2: 型アサーション
const user = {} as { name: string };
user.name = 'John'; // OK

7.2 this コンテキストの扱い

課題: コールバック内での this のバインディングに関する問題。

コード例

class Counter {
  count = 0;
  increment() {
    setTimeout(function() {
      this.count++; // エラー: 'this' が定義されていません
    }, 1000);
  }
}

解決策: アロー関数を使用するか、this を明示的にバインドします。

コード例

// 解決策 1: アロー関数
setTimeout(() => {
  this.count++; // 'this' はレキシカルスコープから継承されます
}, 1000);

// 解決策 2: this をバインドする
setTimeout(function(this: Counter) {
  this.count++;
}.bind(this), 1000);