JavaScript から TypeScript への移行ガイド

2024/02/12に公開

はじめに

JavaScript から TypeScript への移行は、コードの型安全性を向上させ、バグの早期発見を可能にする重要な取り組みです。本記事では、以下の取り組みについて詳しく解説します。

  • JavaScript から TypeScript への移行計画策定
  • コードベースの分析と移行対象ファイルの特定
  • TypeScript 設定ファイル(tsconfig.json)の最適化
  • 型の設計と逐次的な移行手順の作成
  • any 型や型安全性の改善
  • コードのリファクタリングと必要な修正
  • TypeScript を活用したバグの早期発見と修正
  • IDEやCIツールの TypeScript サポート設定
  • 開発チーム向けの TypeScript トレーニング
  • 移行後のテストの強化と改善

移行を円滑に進めるための具体的な手順と、Before / After のサンプルコードを交えて解説します。

移行の目的を明確化

まず、TypeScript への移行の目的を明確にしましょう。一般的な目的として、以下のようなものが挙げられます。

  • 型安全性の向上:ランタイムエラーを減らし、コードの信頼性を高める。
  • 開発効率の向上:IDE の補完機能が強化され、バグの早期発見が可能になる。
  • メンテナンス性の向上:チーム開発において、型情報を活用して意図を明確に伝えやすくする。
  • ライブラリやフレームワークの互換性向上:多くの最新のライブラリが TypeScript をサポートしている。

移行対象ファイルの特定

全てのファイルを一度に TypeScript に変換するのは現実的ではありません。そのため、まずどのファイルを優先的に移行するかを決めることが重要です。

  • 優先度高

    • ユーティリティ関数(共通で使用されるため、型の恩恵を受けやすい)
    • API クライアント(リクエスト/レスポンスの型を定義することで安全性が向上)
    • 型の影響範囲が小さく、独立性の高いモジュール
  • 優先度中

    • 主要なコンポーネント(コンポーネントの props の型を定義することで可読性が向上)
    • ビジネスロジックを含むサービス層
  • 優先度低

    • テストコード(移行後に型の恩恵を得られるが、最優先ではない)
    • 一時的なスクリプトや古いコード

段階的な移行戦略の策定

移行は一気に行うのではなく、段階的に進めるのがベストです。以下のような戦略が考えられます。

  1. tsconfig.json を作成し、部分的な型チェックを導入
  2. .js ファイルを .ts に変更し、エラーを確認
  3. ユーティリティ関数やモデルの型定義を優先的に追加
  4. コンポーネントやビジネスロジック層を順次移行
  5. any を減らし、型の厳密さを高める
  6. 最終的にすべての .js ファイルを .ts に変換し、TypeScript の strict モードを適用

必要なライブラリやツールの確認

TypeScript への移行には、いくつかのライブラリやツールが必要です。事前にインストールしておきましょう。

  • TypeScript 本体
    npm install --save-dev typescript
    
  • 型定義パッケージ(@types)
    • @types/react, @types/node など、プロジェクトに応じた型定義を追加
    npm install --save-dev @types/react @types/node
    
  • ESLint + TypeScript の設定
    npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
    
    • eslintrc.json の設定
    {
      "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
      "parser": "@typescript-eslint/parser",
      "plugins": ["@typescript-eslint"],
      "rules": {
        "@typescript-eslint/no-explicit-any": "warn",
        "@typescript-eslint/explicit-module-boundary-types": "off"
      }
    }
    
  • Jest(テスト環境)と ts-jest の設定
    npm install --save-dev jest ts-jest @types/jest
    

これらを整えておくことで、スムーズに TypeScript への移行を進めることができます。

TypeScript環境でJestをセットアップしたブログ記事もありますので合わせてご参考ください。

https://shinagawa-web.com/blogs/jest-unit-testing-introduction

TypeScript 設定ファイル(tsconfig.json)の最適化

TypeScript の設定ファイル tsconfig.json は、プロジェクトのコンパイル設定を制御する重要なファイルです。

適切に設定することで、型安全性を向上させ、開発時のエラーを未然に防ぐことができます。

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

compilerOptions(コンパイラオプション)

コンパイラの動作を制御するオプションを設定します。

strict: true

  • TypeScript の厳格な型チェックを有効にします。
  • 以下の strict* 系オプションをすべて true に設定するのと同等です。
  • 推奨: true(型安全性を最大限に保つ)

noImplicitAny: true

  • 暗黙的な any 型を禁止します。
  • 型の明示が必要になり、型安全性が向上します。
  • 推奨: true

strictNullChecks: true

  • null や undefined を適切に型として扱うようにします。
  • 例: string | null 型の変数に null を代入できるが、string 型の変数には null を代入できない。
  • 推奨: true(null の取り扱いミスを防ぐ)

strictFunctionTypes: true

  • 関数の引数や戻り値の型チェックを厳格にします。
  • 型の互換性のルールが強化され、意図しない関数の使用ミスを防ぐことができます。
  • 推奨: true

moduleResolution: "node"

  • Node.js スタイルのモジュール解決を行います(node_modules からのパス解決を可能にする)。
  • import 文を使って外部パッケージを適切にインポートできます。
  • 推奨: "node"(一般的な TypeScript プロジェクトではデフォルト)

esModuleInterop: true

  • default export を含む ES モジュールとの互換性を提供します。
  • CommonJS (require) を使うライブラリと import の互換性を確保します。
  • 推奨: true(ES モジュールと互換性を確保するため)

include(対象ディレクトリ)

コンパイル対象の TypeScript ファイルを含むディレクトリを指定します。
"src" を指定することで、src/ 内の .ts や .tsx ファイルのみを対象にできます。

"include": ["src"]

exclude(除外ディレクトリ)

TypeScript のコンパイル対象から除外するディレクトリを指定します。
"node_modules" や dist/ など、コンパイル不要なファイルを指定するのが一般的です。

"exclude": ["node_modules", "dist"]

その他、推奨オプション

プロジェクトによっては、下記のオプションをお勧めします。

forceConsistentCasingInFileNames: true

  • 大文字・小文字の違いによるファイル名の衝突を防ぎます(特に Windows 環境で有効)。
  • 推奨: true

noUnusedLocals: true

  • 使用されていないローカル変数があるとエラーにします。
  • 推奨: true(不要な変数を削除し、コードをクリーンに保つ)

noUnusedParameters: true

  • 使用されていない関数の引数があるとエラーにします。
  • 推奨: true(不要な引数の削除を促し、コードの明瞭性を向上)

noFallthroughCasesInSwitch: true

  • switch 文の case で break を忘れた場合にエラーを出します。
  • 推奨: true(バグを防ぐ)

型の設計と逐次的な移行手順

型の設計は移行の成功を左右します。以下の手順で進めましょう。

  1. .js ファイルを .ts に変更
  2. any を避け、基本的な型を定義
  3. 型推論を活用しつつ、適切な型を適用
  4. 関数の引数と戻り値に型を追加

Before:

function sum(a, b) {
  return a + b;
}

After:

function sum(a: number, b: number) {
  return a + b;
}

any 型や型安全性の改善

最初は any を使いたくなることもありますが、可能な限り避けて、適切な型を適用していきます。

Before:

function getUserData(): any {
  return fetch('/api/user').then(res => res.json());
}

After:

type User = {
  id: number;
  name: string;
  email: string;
};

async function getUserData(): Promise<User> {
  const response = await fetch('/api/user');
  return response.json();
}

コードのリファクタリングと必要な修正

コードを整理し、可読性やメンテナンス性を向上させるために、以下のリファクタリングを実施していきます。

requireimport に置き換え

対象: CommonJS (require) を使用しているコード
対応: ES Modules (import) に書き換える

Before (CommonJS)

const util = require("./util");

After (ES Modules)

import util from "./util";

理由

  • import は静的解析が可能で、ツリーシェイキングの恩恵を受けやすい
  • TypeScript との相性が良く、型推論がしやすい
  • ES Modules は将来的に標準となるため、統一することで可読性・保守性が向上する

クラスベースのコードを関数型コンポーネントに変換

対象: class を使った React コンポーネント
対応: React Hooks を使った関数型コンポーネントにリファクタリング

Before (クラスベースコンポーネント)

import React, { Component } from "react";

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increase</button>
      </div>
    );
  }
}

export default Counter;

After (関数型コンポーネント)

import { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase</button>
    </div>
  );
};

export default Counter;

理由

  • 関数型コンポーネントはクラスベースよりもシンプルで、可読性が高い
  • useState などの React Hooks を活用でき、ロジックの再利用が容易になる
  • クラスの this を意識する必要がなくなり、バグを減らせる

型定義を interfacetype で整理

対象: PropTypes を使用しているコード、または型のないコード
対応: TypeScript の interface や type を活用

Before (PropTypes 使用)

import PropTypes from "prop-types";

const UserCard = ({ name, age }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
  </div>
);

UserCard.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
};

export default UserCard;

After (TypeScript interface 使用)

interface UserCardProps {
  name: string;
  age?: number;
}

const UserCard: React.FC<UserCardProps> = ({ name, age }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
  </div>
);

export default UserCard;

理由

  • PropTypes は実行時の型チェックだが、TypeScript ならコンパイル時にエラーを検出できる
  • interface や type を使うことで、型情報がIDEで補完され、開発効率が向上する
  • 型の定義を統一することで、コードの可読性・保守性が向上する

TypeScript を活用したバグの早期発見と修正

TypeScript を導入すると、コンパイル時にバグを発見できます。例えば、以下のようなエラーを防げます。

Before:

const user = getUserData();
console.log(user.name.toUpperCase()); // user が null ならエラー

After:

const user: User | null = await getUserData();
console.log(user?.name.toUpperCase());

IDE や CI ツールの TypeScript サポート設定

TypeScript を導入すると、開発体験やコードの品質向上のために IDE と CI の設定を最適化することが重要です。
ここでは、VS Code の ESLint 設定と、GitHub Actions での型チェックについて詳しく説明します。

VS Code の eslint + typescript-eslint 設定

VS Code で TypeScript コードを静的解析し、型チェックやコーディングスタイルを統一するために、ESLint と @typescript-eslint を設定します。

手順

  1. ESLint と TypeScript ESLint パッケージをインストール
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
  1. ESLint 設定ファイル .eslintrc.js を作成
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
  ],
  rules: {
    '@typescript-eslint/no-unused-vars': ['error'],
    '@typescript-eslint/explicit-function-return-type': 'off',
  },
};
  1. VS Code に ESLint 拡張機能をインストール

  2. VS Code の settings.json に ESLint を有効化

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.validate": ["typescript", "typescriptreact"]
}

動作確認
以下のコマンドで ESLint が正しく動作するか確認。

npx eslint src/**/*.ts --fix

GitHub Actions で tsc --noEmit を実行し型チェック

手順

  1. GitHub Actions のワークフロー定義を作成 .github/workflows/ci.yml を作成し、tsc --noEmit で型チェックを行う。
name: TypeScript Check

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v4

      - name: Node.js をセットアップ
        uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'npm'

      - name: 依存関係をインストール
        run: npm ci

      - name: TypeScript の型チェックを実行
        run: npx tsc --noEmit

GitHub に push すると、GitHub Actions 上で tsc --noEmit が実行され、型エラーがあれば検出される。

開発チーム向けの TypeScript トレーニング

チームメンバーが TypeScript をスムーズに使えるように、以下の取り組みを推奨します。

TypeScript の基本概念を学ぶ(型推論、ユニオン型、ジェネリクスなど)

まずは TypeScript の主要な概念を理解し、実際に手を動かして学ぶことが重要です。

型推論(Type Inference)

  • TypeScript は明示的な型指定をしなくても、ある程度の型を自動推論する
let message = "Hello, TypeScript"; // string 型として推論
let count = 10; // number 型として推論

ユニオン型(Union Types)

  • 変数に複数の型を許容する
function printId(id: string | number) {
  console.log(`Your ID is: ${id}`);
}

printId(123);  // OK
printId("abc");  // OK

ジェネリクス(Generics)

  • 汎用的な型を定義し、型の柔軟性を確保
function identity<T>(arg: T): T {
  return arg;
}

let result = identity<number>(10);  // 型: number
let text = identity<string>("Hello");  // 型: string

型エイリアス(Type Aliases)とインターフェース(Interfaces)

  • 型の再利用性を向上させる
type User = {
  id: number;
  name: string;
};

interface Product {
  id: number;
  name: string;
  price: number;
}

const user: User = { id: 1, name: "Alice" };
const product: Product = { id: 1, name: "Laptop", price: 1000 };

チーム内で TypeScript のベストプラクティスを共有

実践的な場面での統一したコーディングスタイルやベストプラクティスを共有し、チームの生産性を向上させます。

共有すべきベストプラクティス

  • 型推論を活用しつつ、明示的に型を指定すべき箇所を見極める

    • 型推論を活かしつつ、曖昧な型(any)を避ける
  • 不必要な any の使用を避ける

    • noImplicitAny を有効にすることで未定義の any を防ぐ
    • unknown や never を活用し、厳格な型チェックを実施
  • 関数の引数と戻り値に明示的な型を指

function add(a: number, b: number): number {
  return a + b;
}
  • 型の安全性を意識したエラーハンドリング
function fetchData(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data received");
    }, 1000);
  });
}

async function main() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error("Error:", error);
  }
}

レビューで型の改善を促進

コードレビューの際に型の適用をチェックし、より良い書き方をチーム内で共有します。

型に関するレビューのポイント

  • ✅ 適切な型が指定されているか?(不要な any の使用を避ける)
  • ✅ 冗長な型指定がないか?(型推論を適切に活用できているか)
  • ✅ null や undefined の考慮がされているか?
  • ✅ ユニオン型・ジェネリクスの適用が適切か?

例: 型を改善するコードレビュー

// ❌ 改善前: any を使用
function getUser(id: any): any {
  return { id, name: "John Doe" };
}

// ✅ 改善後: 明確な型を指定
function getUser(id: number): { id: number; name: string } {
  return { id, name: "John Doe" };
}

実践的な学習方法

  1. ハンズオン(Hands-on)ワークショップ

    • 実際に TypeScript のコードを書きながら学習
    • 例: 「型エイリアス vs インターフェースの使い分け」
  2. ペアプログラミング

    • 経験者とペアを組んでコーディングしながら学ぶ
  3. 定期的な勉強会

    • 週1回の TypeScript ベストプラクティス共有会を開催
    • 例: 「ユニオン型とインターセクション型の活用」
  4. 実際のプロジェクトで適用

    • 小さなリファクタリングから始めて、段階的に TypeScript を導入

移行後のテストの強化と改善

TypeScript の導入により、テストの信頼性も向上します。

  • Jest や Testing Library で型付きのテストを書く
  • ts-jest を導入し TypeScript ベースのテストを実施
  • @types/jest をインストールし型チェックを強化

Before:

test('should return user name', () => {
  expect(getUserData().name).toBe('Alice');
});

After:

test('should return user name', async () => {
  const user = await getUserData();
  expect(user.name).toBe('Alice');
});

まとめ

本記事では、JavaScript から TypeScript への移行について、以下の観点から詳しく解説しました。

  • JavaScript から TypeScript への移行計画策定
  • コードベースの分析と移行対象ファイルの特定
  • TypeScript 設定ファイル(tsconfig.json)の最適化
  • 型の設計と逐次的な移行手順の作成
  • any 型や型安全性の改善
  • コードのリファクタリングと必要な修正
  • TypeScript を活用したバグの早期発見と修正
  • IDEやCIツールの TypeScript サポート設定
  • 開発チーム向けの TypeScript トレーニング
  • 移行後のテストの強化と改善

JavaScript から TypeScript への移行は一朝一夕ではありませんが、計画的に進めることで確実に型安全性を向上させることができます。本記事で紹介したステップを参考に、少しずつ移行を進めてみてください。

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ