【TypeScript】JavaScriptからTypeScriptへの移行完全ガイド|段階的移行・tsconfig設定・型エラー解消・実務パターン徹底解説

【TypeScript】JavaScriptからTypeScriptへの移行完全ガイド|段階的移行・tsconfig設定・型エラー解消・実務パターン徹底解説 TypeScript

今のJavaScriptプロジェクトにTypeScriptを導入したい」——しかし、既存コードが大量にあると何から手をつければいいか迷いますよね。いきなり全ファイルを書き換えようとすると、大量の型エラーが噴出してプロジェクトが止まってしまいます。

この記事では、実務で使える段階的な移行戦略を軸に、tsconfig の設定・よく発生するエラーの解消法・外部ライブラリの型定義まで、JavaScriptからTypeScriptへの移行を体系的に解説します。

この記事でわかること

  • 3つの移行戦略(段階移行・一括移行・書き直し)の選び方
  • allowJs を使った段階的移行の具体的な手順
  • 移行時に頻発する型エラーと解消パターン
  • strict: true を段階的に有効にする方法
  • 外部ライブラリの型定義(@types / declare module)の追加方法
  • 既存コードを壊さず型安全性を高める実務パターン
スポンサーリンク

TypeScriptへ移行するメリット・デメリット

観点 メリット デメリット
型安全性 バグをコンパイル時に検出。null/undefinedのランタイムエラーが激減 移行初期に大量の型エラーが出る
開発体験 IDEの補完・リファクタリングが強化される。コードナビゲーションが向上 ビルドステップが増え、初期学習コストがある
ドキュメント性 型が仕様書の役割を果たす。コードを読むだけで意図がわかる 詳細な型定義を書くのに時間がかかる場合がある
チーム開発 型によってAPIの誤用を防げる。大規模チームで特に効果的 全員がTypeScriptを学ぶ必要がある
エコシステム 主要ライブラリの多くが@typesを提供 型定義がないライブラリは自前で書く必要がある
移行は「一気に」ではなく「段階的に」が鉄則
大規模プロジェクトを一度に移行しようとすると、数百〜数千の型エラーが発生し、開発が長期間止まります。まず動くものを維持しながら、少しずつ型安全性を高めるのが成功の鍵です。

移行戦略の選択

プロジェクトの規模・チームの状況に応じて3つの戦略から選びます。

戦略 概要 向いているケース 主なリスク
段階移行(推奨) allowJs: true でJSとTSを共存させながら徐々に移行 大規模プロジェクト・チームでの作業 移行期間が長くなる
一括移行 全ファイルを一度に.tsに変換 小〜中規模プロジェクト・個人開発 大量の型エラーが一度に出る
書き直し TypeScriptで新規プロジェクトを立ち上げ、機能を移植 レガシーコードが複雑すぎる場合 工数が大幅に増える

環境セットアップ

TypeScript のインストール

# TypeScript 本体
npm install --save-dev typescript

# Node.js 環境の場合は型定義も追加
npm install --save-dev @types/node

# バージョン確認
npx tsc --version

tsconfig.json の初期作成

# tsconfig.json を自動生成
npx tsc --init

生成された tsconfig.json をプロジェクトに合わせてカスタマイズします。詳しい全オプションはtsconfig.json完全ガイドを参照してください。

段階移行の手順(allowJs を使う方法)

最も安全な移行戦略は allowJs: true を使い、JavaScriptファイルとTypeScriptファイルを共存させながら少しずつ移行する方法です。

Step 1:tsconfig.json を移行モードに設定

// tsconfig.json(移行初期設定)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",

    // ── 移行用オプション ──
    "allowJs": true,          // .js ファイルをコンパイル対象に含める
    "checkJs": false,         // JS ファイルの型チェックは最初は無効
    "strict": false,          // strict は最初は無効(後で段階的に有効化)
    "noImplicitAny": false,   // 暗黙的 any を最初は許可

    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
allowJs 使用時の rootDir エラーに注意
allowJs: true にして rootDir を指定すると、”File 'xxx.js' is not under 'rootDir'” というエラーが出ることがあります。原因は .js と .ts ファイルが混在しているときに TypeScript が共通の親ディレクトリを推論するためです。解決策は "rootDir" を削除するか、"include""outDir" だけで管理することです。あるいは "composite": false と組み合わせて "rootDir" を明示的に "." にする方法もあります。
allowJs と checkJs の違い
allowJs: true.js ファイルをコンパイル対象にする(型チェックはしない)。JS ファイルから TS ファイルへのインポートが可能になる。
checkJs: true.js ファイルにも型チェックを実施する(allowJs が前提)。移行が進んだ段階で有効にすると、残りの JS ファイルのエラーを検出できる。

Step 2:ファイルを 1 つずつ .ts にリネームする

優先度の高いファイル(コアロジック・頻繁に変更するファイル)から順に .js.ts に変更します。

// ❶ まずファイルをリネーム
//   utils.js → utils.ts
//   userService.js → userService.ts

// ❷ コンパイルエラーを確認
// npx tsc --noEmit

// ❸ エラーを修正(最初は any でも OK)
// function getUser(id: any): any {
//   ...
// }

// ❹ 動作確認後、次のファイルへ
最初から完璧を目指さない
移行初期は any を使って型エラーを一時的に抑制するのは正常なプロセスです。まず動くコードを維持することを最優先にし、その後で any を具体的な型に置き換えていきましょう。// @ts-ignore// @ts-expect-error を一時的に使うのも有効です。
@ts-ignore より @ts-expect-error を使う
// @ts-ignore は次の行のエラーをすべて無視しますが、エラーが解消されても自動的に削除されません(残骸になる)。// @ts-expect-error(TypeScript 3.9+)は、もしエラーがなくなった場合に「@ts-expect-error が不要です」というエラーを出してくれます。これにより移行が進んで型が付いたときに抑制コメントを確実に除去できます。移行時は @ts-expect-error を優先して使いましょう。

Step 3:checkJs を有効にして残りの JS もチェック

// tsconfig.json(移行中盤)
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,   // JS ファイルにも型チェックを適用
    "strict": false,
    ...
  }
}

// JS ファイルで型エラーを抑制したい場合は JSDoc を使う
/**
 * @param {string} name
 * @returns {string}
 */
function greet(name) {
  return `Hello, ${name}!`;
}

// または @ts-check を先頭に記述してファイル単位で有効化
// @ts-check

Step 4:strict モードを段階的に有効化

すべての .js.ts 変換が終わったら、strict 関連オプションを1つずつ有効にします。

// 段階1: noImplicitAny から始める
{
  "compilerOptions": {
    "noImplicitAny": true,   // 暗黙的 any を禁止
    "strictNullChecks": false
  }
}

// 段階2: strictNullChecks を有効化
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true  // null/undefined の型安全性
  }
}

// 段階3: strict: true で全て有効化(最終目標)
{
  "compilerOptions": {
    "strict": true  // noImplicitAny + strictNullChecks + その他を全て有効
  }
}
strict: true が含む主なチェック
strict: true は以下をすべて有効にします:noImplicitAny(暗黙的anyを禁止)・strictNullChecks(null/undefinedの安全性)・strictFunctionTypes(関数型の共変性チェック)・strictBindCallApply(bind/call/applyの型チェック)・strictPropertyInitialization(クラスプロパティの初期化チェック)。一度に有効にするのが難しい場合は、上記のように個別に有効にしていきましょう。

移行時によく発生する型エラーと解消パターン

パターン1:暗黙的 any(Parameter implicitly has an ‘any’ type)

// ❌ noImplicitAny: true にすると発生
function multiply(a, b) {       // Error: パラメータに暗黙的に any 型が含まれる
  return a * b;
}

// ✅ 修正: 型アノテーションを追加
function multiply(a: number, b: number): number {
  return a * b;
}

// コールバック関数も同様
// ❌
const items = [1, 2, 3];
items.forEach((item) => {   // item の型は推論されるので OK(配列の型から決まる)
  console.log(item);
});

// ❌ 外部からのデータは型が推論できない
function processData(data) {    // Error: any 型
  return data.value;
}

// ✅ 型定義を作成してアノテーション
interface DataItem {
  value: string;
  id: number;
}
function processData(data: DataItem): string {
  return data.value;
}

パターン2:null/undefined エラー(Object is possibly null)

// ❌ strictNullChecks: true にすると発生
function getLength(str: string | null): number {
  return str.length; // Error: Object is possibly 'null'
}

// ✅ 修正パターン1: if チェック
function getLength(str: string | null): number {
  if (str === null) return 0;
  return str.length;
}

// ✅ 修正パターン2: オプショナルチェーン
function getLength(str: string | null): number {
  return str?.length ?? 0;
}

// DOM 操作でよくある例
// ❌
const input = document.getElementById("myInput");
input.value = "hello";  // Error: Object is possibly null

// ✅ 修正パターン1: null チェック
const input = document.getElementById("myInput");
if (input instanceof HTMLInputElement) {
  input.value = "hello"; // OK
}

// ✅ 修正パターン2: Non-null assertion(確実に存在するとわかる場合のみ)
const input = document.getElementById("myInput") as HTMLInputElement;
input.value = "hello";

null/undefined の絞り込み方法については型の絞り込み(Type Narrowing)完全ガイドで詳しく解説しています。

パターン3:プロパティが存在しない(Property does not exist on type)

// ❌ オブジェクトの型推論でプロパティが認識されない
const user = {
  name: "Alice",
  age: 30,
};
user.email = "alice@example.com"; // Error: Property 'email' does not exist

// ✅ 修正1: 型定義を使う
interface User {
  name: string;
  age: number;
  email?: string; // オプショナルプロパティ
}
const user: User = { name: "Alice", age: 30 };
user.email = "alice@example.com"; // OK

// ✅ 修正2: インデックスシグネチャ(動的なキーを許可)
interface Config {
  [key: string]: string | number;
}
const config: Config = {};
config.apiUrl = "https://api.example.com"; // OK
config.timeout = 5000;                     // OK

パターン4:型の不一致(Argument of type X is not assignable to type Y)

// ❌ 文字列を number として使う
function double(n: number): number { return n * 2; }
const input = "42"; // string
double(input);      // Error: Argument of type 'string' is not assignable to 'number'

// ✅ 修正: 型変換
double(Number(input));    // 明示的な変換
double(parseInt(input));  // 整数に変換
double(+input);           // 単項プラスで変換

// ❌ API レスポンスの型不一致(よくあるパターン)
interface ApiResponse {
  data: User[];
  total: number;
}
async function fetchUsers(): Promise<User[]> {
  const res = await fetch("/api/users");
  const json = await res.json(); // any 型
  return json; // ❌ any は User[] に代入できない(strict 環境)
}

// ✅ 修正: 型アサーションでキャスト
async function fetchUsers(): Promise<User[]> {
  const res  = await fetch("/api/users");
  const json = await res.json() as ApiResponse;
  return json.data;
}

パターン5:モジュールが見つからない(Cannot find module)

// ❌ よくある原因1: @types パッケージがない
import express from "express"; // Error: Cannot find module 'express'

// ✅ 修正: @types パッケージをインストール
// npm install --save-dev @types/express

// ❌ よくある原因2: パスエイリアスが tsconfig で設定されていない
import { UserService } from "@/services/user"; // Error

// ✅ 修正: tsconfig.json に paths を設定
// {
//   "compilerOptions": {
//     "baseUrl": "./src",
//     "paths": {
//       "@/*": ["./*"]
//     }
//   }
// }

// ❌ よくある原因3: 型定義ファイルがないサードパーティライブラリ
import someLib from "some-lib-without-types"; // Error

// ✅ 修正: declare module で型を宣言(後述)

外部ライブラリの型定義

@types パッケージのインストール

多くのライブラリは @types/ライブラリ名 という別パッケージで型定義を提供しています。

# よく使う @types パッケージ
npm install --save-dev @types/node      # Node.js
npm install --save-dev @types/express   # Express
npm install --save-dev @types/react     # React
npm install --save-dev @types/react-dom # React DOM
npm install --save-dev @types/lodash    # lodash
npm install --save-dev @types/jest      # Jest

# @types が存在するか確認
# https://www.npmjs.com/ で "@types/ライブラリ名" を検索
@types が不要なライブラリもある
近年は TypeScript で書かれたライブラリが増え、パッケージ本体に型定義(.d.ts)が含まれていることが多いです。インストール後にエラーが出なければ @types は不要です。エラーが出た場合のみ @types を確認してください。

型定義がないライブラリの対処法

// ❌ @types もなく型定義ファイルもないライブラリ
// エラー: Could not find a declaration file for module 'legacy-lib'

// ✅ 対処法1: declare module で簡易型定義を作成
// src/types/legacy-lib.d.ts
declare module "legacy-lib" {
  export function doSomething(value: string): void;
  export const version: string;
  export default function main(): void;
}

// ✅ 対処法2: any で型チェックをスキップ(一時的な対処)
declare module "legacy-lib"; // 全エクスポートを any として扱う

// ✅ 対処法3: tsconfig.json で特定ライブラリをチェック対象外にする
// "skipLibCheck": true  ← ライブラリの型定義ファイルのチェックをスキップ

型定義ファイル(.d.ts)の詳しい書き方は型定義ファイル(.d.ts)完全ガイドを参照してください。

CommonJS から ES Modules への移行

Node.js プロジェクトでは require() / module.exports からimport / export への変換が必要になることがあります。

// ── Before: CommonJS ──
const fs = require("fs");
const { join } = require("path");
const express = require("express");

module.exports = { greet };
module.exports.default = App;

// ── After: ES Modules (TypeScript) ──
import fs from "fs";
import { join } from "path";
import express from "express";

export { greet };
export default App;

// ── tsconfig.json での設定 ──
// CommonJS のまま使い続ける場合
// "module": "commonjs"
// "esModuleInterop": true  ← require スタイルのデフォルトインポートを許可

// ES Modules に移行する場合
// "module": "ESNext"
// package.json に "type": "module" も必要
esModuleInterop は必ず true にする
esModuleInterop: true がないと、CommonJS ライブラリの import で問題が起きることがあります。例:import express from "express" が動かず import * as express from "express" と書く必要が生じます。npx tsc --init で生成した tsconfig には esModuleInterop: true がデフォルトで含まれています。

JSDoc による段階的型付け

.js ファイルを .ts に変換せずに型情報を付与する方法として、JSDoc アノテーションがあります。既存の JS ファイルを触らずに型チェックだけ有効にしたい場面で使えます。

// @ts-check を先頭に追加するだけで型チェックが有効になる
// @ts-check

/**
 * ユーザーを作成する
 * @param {string} name - ユーザー名
 * @param {number} age - 年齢
 * @returns {{ id: number, name: string, age: number }}
 */
function createUser(name, age) {
  return { id: Math.random(), name, age };
}

/**
 * @typedef {Object} Product
 * @property {number} id
 * @property {string} name
 * @property {number} price
 */

/**
 * @param {Product[]} products
 * @returns {number}
 */
function calcTotal(products) {
  return products.reduce((sum, p) => sum + p.price, 0);
}

// TypeScript ファイルからも型情報を参照できる
// @type {import("./userService").UserService}
const userService = require("./userService");
JSDoc型付けはゆるやかな移行に最適
JSDocアノテーションを使うと、.js ファイルのまま型チェックの恩恵を受けられます。チームが TypeScript に不慣れな場合や、移行工数を最小にしたい場合に有効です。ただし TypeScript の型システムの全機能(ジェネリクス・条件型等)は使えません。最終的には .ts への移行が理想です。

実務でよく使う移行パターン

パターン1:オブジェクトを interface / type で型付けする

// ── Before: JavaScript ──
function formatUser(user) {
  return `${user.firstName} ${user.lastName} (${user.email})`;
}

// ── After: TypeScript ──
interface User {
  id:        number;
  firstName: string;
  lastName:  string;
  email:     string;
  createdAt: Date;
}

function formatUser(user: User): string {
  return `${user.firstName} ${user.lastName} (${user.email})`;
}

// APIレスポンスの型付け(よくある移行パターン)
interface ApiUser {
  id:         number;
  first_name: string; // スネークケース(API仕様)
  last_name:  string;
  email:      string;
}

function transformUser(apiUser: ApiUser): User {
  return {
    id:        apiUser.id,
    firstName: apiUser.first_name,
    lastName:  apiUser.last_name,
    email:     apiUser.email,
    createdAt: new Date(),
  };
}

パターン2:関数の戻り値型を明示する

// ── Before: JavaScript ──
function getUserById(id) {
  return users.find(u => u.id === id) || null;
}

// ── After: TypeScript ──
// 戻り値型を明示することで、呼び出し側が null チェックを強制される
function getUserById(id: number): User | null {
  return users.find(u => u.id === id) ?? null;
}

// 非同期関数も同様
// ── Before ──
async function fetchUsers() {
  const res = await fetch("/api/users");
  return res.json();
}

// ── After ──
async function fetchUsers(): Promise<User[]> {
  const res = await fetch("/api/users");
  return res.json() as User[];
}

パターン3:クラスをTypeScriptに移行する

// ── Before: JavaScript ──
class UserService {
  constructor(db) {
    this.db = db;
  }

  async findById(id) {
    return this.db.query("SELECT * FROM users WHERE id = ?", [id]);
  }

  async create(name, email) {
    return this.db.query(
      "INSERT INTO users (name, email) VALUES (?, ?)",
      [name, email]
    );
  }
}

// ── After: TypeScript ──
interface Database {
  query<T>(sql: string, params?: unknown[]): Promise<T>;
}

class UserService {
  constructor(private db: Database) {}

  async findById(id: number): Promise<User | null> {
    const rows = await this.db.query<User[]>(
      "SELECT * FROM users WHERE id = ?",
      [id]
    );
    return rows[0] ?? null;
  }

  async create(name: string, email: string): Promise<User> {
    return this.db.query<User>(
      "INSERT INTO users (name, email) VALUES (?, ?)",
      [name, email]
    );
  }
}

移行チェックリスト

フェーズ チェック項目 優先度
環境構築 TypeScript と @types/node をインストール
環境構築 tsconfig.json を作成(allowJs: true, strict: false)
環境構築 ビルドスクリプトを tsc コマンドに変更
ファイル変換 コアロジックの .js → .ts を優先的にリネーム
ファイル変換 型エラーを確認(npx tsc –noEmit)
ファイル変換 暗黙的 any を interface / type で解消
ライブラリ型定義 使用ライブラリの @types パッケージをインストール
ライブラリ型定義 型定義のないライブラリは declare module で宣言
strict 化 noImplicitAny: true を有効化してエラー解消
strict 化 strictNullChecks: true を有効化してエラー解消
strict 化 strict: true を有効化(最終目標)
品質向上 any 型を使った箇所を具体的な型に置き換え
品質向上 CI/CD に型チェック(tsc –noEmit)を追加

よくある質問

Q既存の大量の JavaScript コードを短期間で移行するコツはありますか?

AallowJs: true + strict: false の設定で移行を始め、新規追加するファイルはすべて .ts で書くというルールにするのが現実的です。既存の .js ファイルは優先度をつけて少しずつ移行します。焦って一気に変換しようとするとデグレが起きやすいため、段階的に進めることが成功の鍵です。

Qtsconfig.json の strict: true にすると大量のエラーが出て困っています。

Astrict: true は1つのオプションに見えますが、実際には複数のチェックをまとめて有効にしています。まず noImplicitAny: true だけを有効にしてエラーを解消し、次に strictNullChecks: true、というように1つずつ有効にしていくことをお勧めします。各エラーの解消方法はTypeScriptエラー解決ガイドを参照してください。

Qany 型はできる限り使わないほうがよいですか?

A移行中は一時的に any を使うのは問題ありません。しかし最終的には any を排除することを目指してください。any の代わりに unknown を使うと型チェックを維持しつつ柔軟に扱えます。unknown vs any の違いはunknown・any・never の違いで詳しく解説しています。

QReact プロジェクトを TypeScript に移行する場合、特別に考慮すべき点はありますか?

AReact の場合は @types/react@types/react-dom を追加した上で、.jsx.tsx.js.ts にリネームします。tsconfig に "jsx": "react-jsx"(React 17+)または "jsx": "react"(旧版)を追加してください。Props の型定義と Hooks の型付けについてはTypeScript React ガイドを参照してください。

QJavaScript → TypeScript の移行を自動化するツールはありますか?

Ats-migrate(Airbnb製)が代表的です。自動的に .js.ts に変換し、型エラーを // @ts-expect-error で抑制してくれます。これにより移行初日からコンパイルが通る状態にでき、その後 // @ts-expect-error を1つずつ解消していく流れになります。ただし自動変換の精度には限界があるため、重要なロジックは手動で確認することをお勧めします。

まとめ

JavaScript から TypeScript への移行は、一度に完璧を目指すのではなく、段階的に進めることが成功の鍵です。

移行フェーズ 主な作業 目標
準備 TypeScript インストール・tsconfig 設定(allowJs, strict: false) ビルドが通る状態を維持
変換 .js → .ts リネーム・型エラーの修正(any で仮置き可) すべてのファイルを .ts 化
型強化 noImplicitAny → strictNullChecks → strict: true を段階的に有効化 any を排除・型安全性を最大化
品質向上 @types 追加・declare module・CI への型チェック追加 型定義を完全化

まずは小さなプロジェクトや新しいファイルから TypeScript を書き始め、慣れてきたら既存コードの移行に取り組むのがよいでしょう。tsconfig.json の詳細設定型の絞り込み型定義ファイルも合わせて参照してください。