TypeScript JS プロジェクトを TS へ移行する
既存のJavaScriptプロジェクトをTypeScriptに移行することは、最も一般的かつ実用的なシナリオです。このレッスンでは、その移行を安全かつ段階的に完了させる方法について解説します。
1. 移行戦略の概要
(1) 3つの移行戦略
| 戦略 | スピード | リスク | 適したシナリオ |
|---|---|---|---|
| 1回限りの完全移行 | 高速 | 高 | 小規模プロジェクト(20ファイル未満) |
| ファイル単位の段階的な移行 | 中規模 | 小規模 | 中規模~大規模プロジェクト(推奨) |
| JSDoc の段階的な移行 | 低速 | 非常に低速 | 大規模/重要なプロジェクト |
📌 推奨方法:ファイル単位での段階的な移行。 まず、TSコンパイラがJSファイルを受け入れるように設定し、その後、ファイルの拡張子を.jsから.tsへと1つずつ変更していきます。その際、ファイルを変更するたびにコンパイルが正常に完了することを確認してください。
(2) 移行手順の概要
TEXT
1. Initialization tsconfig.json(Relaxed Mode)
2. Install @types packages
3. Open allowJs——TS and JS Coexistence
4. File by file .js → .ts(Starting with the leaf file)
5. Gradually Enable Strict Options
6. Finally Unlocked strict
2. 手順 1: 設定の初期化
(1) tsconfig.json を作成する
BASH
tsc --init
(2) 主要な初期設定—リラックスモード
JSON
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"allowJs": true,
"checkJs": false,
"noImplicitAny": false,
"strict": false,
"outDir": "./dist",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
💡 重要なポイント: 移行の初期段階では、
strict や noImplicitAny を有効にしないでください。まずプロジェクトがコンパイルできることを確認してから、徐々に制限を厳格化してください。
(3) 設置形態の申告
BASH
# Install the type declarations required by the project
npm install @types/node @types/express @types/lodash --save-dev
3. 手順 2: allowJs を使用して JS と TS の共存を有効にする
(1) allowJs を有効にする
JSON
{
"compilerOptions": {
"allowJs": true
}
}
allowJs を使用すると、TypeScript コンパイラが .js ファイルを受け入れるようになります。これにより、プロジェクト内に JS ファイルと TS ファイルを混在させても、互いに影響し合うことはありません。
(2) インポートの互換性
TYPESCRIPT
// main.ts —— Can be imported .js Documents
import { helper } from "./utils"; // utils.js Just being there is enough
// utils.js —— JS Documents,Untyped Annotations
export function helper(value) {
return value.toString();
}
(3) checkJs—オプションのJavaScript型チェック
JSON
{
"compilerOptions": {
"checkJs": true
}
}
checkJs が有効になっている場合、TypeScript は .js ファイル内の型エラーも(JSDoc コメントと型推論に基づいて)チェックします。初期の移行段階では、これを無効にしておくことをお勧めします。JS ファイルが段階的に TS に変換された後にのみ、有効にしてください。
4. 手順 3:ファイルを 1 つずつ移行する
(1) 移行手順
まず、依存関係が最も少ないファイル、つまり「リーフ」ファイル(他のプロジェクトからファイルをインポートせず、ユーティリティ関数や定数などを含むファイル)から始めましょう:
TEXT
Recommended Migration Order:
1. Constants File(config.js → config.ts)
2. Utility Functions(utils.js → utils.ts)
3. Type Definitions(types.js → types.ts)
4. Data Model(models.js → models.ts)
5. Service Layer(services.js → services.ts)
6. Controller/Routing(controllers.js → controllers.ts)
7. Input File(index.js → index.ts)
(2) 単一ファイルの移行手順
TYPESCRIPT
// ── Before the Migration:utils.js ──
export function formatPrice(price, currency) {
return currency + price.toFixed(2);
}
export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
TYPESCRIPT
// ── After the migration:utils.ts ──
export function formatPrice(price: number, currency: string = "¥"): string {
return currency + price.toFixed(2);
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
(3) 移行手法――any から始め、その後微調整を行う
TYPESCRIPT
// Step 1:Add a type annotation,Uncertain usage any
function process(data: any, options: any): any {
return data.filter(item => item.active);
}
// Step 2:Gradual Replacement any For a specific type
interface DataItem {
id: number;
name: string;
active: boolean;
}
interface Options {
limit?: number;
sort?: "asc" | "desc";
}
function process(data: DataItem[], options: Options = {}): DataItem[] {
let result = data.filter(item => item.active);
if (options.sort === "desc") result.reverse();
if (options.limit) result = result.slice(0, options.limit);
return result;
}
▶ 例:Expressのルートファイルの移行
TYPESCRIPT
// ── Before the Migration:users.js ──
const express = require("express");
const router = express.Router();
router.get("/", async (req, res) => {
const users = await User.findAll();
res.json(users);
});
router.post("/", async (req, res) => {
const { name, email } = req.body;
const user = await User.create({ name, email });
res.status(201).json(user);
});
module.exports = router;
TYPESCRIPT
// ── After the migration:users.ts ──
import { Router, Request, Response } from "express";
const router = Router();
interface CreateUserBody {
name: string;
email: string;
}
router.get("/", async (_req: Request, res: Response) => {
const users = await User.findAll();
res.json(users);
});
router.post("/", async (req: Request<{}, {}, CreateUserBody>, res: Response) => {
const { name, email } = req.body;
const user = await User.create({ name, email });
res.status(201).json(user);
});
export default router;
5. JSDocの型アノテーション
ファイルの拡張子を変更したくない場合は、JSDoc を使って JS ファイルに型情報を追加することができます:
(1) 基本型のアノテーション
JAVASCRIPT
// utils.js — Use JSDoc to add types
/**
* Pricing Format
* @param {number} price - Price
* @param {string} [currency="¥"] - Currency Symbol
* @returns {string} Formatted Price
*/
export function formatPrice(price, currency = "¥") {
return currency + price.toFixed(2);
}
/**
* @typedef {Object} User
* @property {number} id
* @property {string} name
* @property {string} email
*/
/**
* Search for a User
* @param {number} id
* @returns {Promise<User>}
*/
export async function findUser(id) {
// ...
}
(2) TSファイルでのJSDoc型の参照
TYPESCRIPT
// main.ts
import { findUser } from "./utils"; // utils.js has JSDoc types
let user = await findUser(1); // ✅ Type inference is correct(From JSDoc)
(3) 一般的な JSDoc タイプタグ
| タグ | 用途 | 例 |
|---|---|---|
@type |
変数型 | @type {string} |
@param |
パラメータの種類 | @param {number} x |
@returns |
戻り値の型 | @returns {string} |
@typedef |
型の定義 | @typedef {Object} User |
@property |
オブジェクトのプロパティ | @property {string} name |
@template |
汎用パラメータ | @template T |
@callback |
コールバックの種類 | @callback Handler |
6. 移行におけるよくある落とし穴
(1) 落とし穴 1:暗黙の any 爆発
TYPESCRIPT
// A large number of implicit variables after migration any——Don't rush to start it noImplicitAny
function process(data) { // data Implicit any
return data.map(item => item.name); // item Me too any
}
解決策: まず、プロジェクトを正常にコンパイルできるようにし、次に型注釈を1つずつ追加し、最後にnoImplicitAnyを無効にします。
(2) 落とし穴 2:module.exports と import の間の競合
TYPESCRIPT
// JS For use with documents module.exports
module.exports = function() { /* ... */ };
// TS Import Requirements esModuleInterop
import fn from "./legacy"; // Required esModuleInterop: true
(3) 落とし穴 3:型指定のないサードパーティ製ライブラリ
TYPESCRIPT
import untypedLib from "untyped-lib"; // ❌ Cannot find the declaration file
// Temporary solution——Create shim.d.ts
declare module "untyped-lib" {
const lib: any;
export default lib;
}
(4) 落とし穴 4:this 型の喪失
TYPESCRIPT
// JS in this Dynamic Binding
const obj = {
name: "Charlie",
greet() {
console.log(this.name); // ✅ JS in OK
}
};
// TS in this Needs annotation
const obj2 = {
name: "Charlie",
greet(this: { name: string }) {
console.log(this.name); // ✅ TS Needed in this Parameters
}
};
❓ よくある質問
Q 大規模なプロジェクトの移行にはどれくらい時間がかかりますか?
A コードの量やチームによって異なります。中小規模のプロジェクト(10,000行以下)の場合、約1~2週間かかります。大規模なプロジェクト(10万行以上)の場合は、1~3ヶ月かけて段階的に移行する必要がある場合があります。重要なのは、すべての変更を一度に行わないことです。1ファイルずつ移行し、各ステップの後にコードが正常にコンパイルされることを確認してください。
Q JSDocの型注釈とTypeScriptの型注釈、どちらが優れていますか?
A TypeScriptの型注釈の方が優れています。構文がより簡潔で、機能も強力であり、エディタのサポートも充実しています。JSDocは、ファイル拡張子を変更する必要がないという妥協案であり、ファイル名を変更できない状況に適しています。最終的な目標は、やはりJSファイルをTypeScriptに変換することです。
Q 移行中のCIについてはどうなりますか?
A 型チェックのため、CIに
tsc --noEmitを追加します(ファイルの出力は行いません)。移行の初期段階では、型の問題によってCIが失敗しないよう、--noImplicitAny falseの使用が許可されます。プロセスが徐々に厳格化されるにつれて、CIにおける型チェックもますます厳格になっていきます。Q
ts-ignore と ts-expect-error、どちらを使うべきですか?A まずは
@ts-expect-error を使用してください。次の行に型エラーがない場合、エラーが発生します(修正後にコメントを削除し忘れるのを防ぐためです)。@ts-ignoreはエラーを無条件に無視するため、すでに修正済みのエラーが見逃されてしまう可能性があります。どちらも一時的な回避策であり、最終的には型の問題を修正する必要があります。📖 まとめ
- 移行戦略:段階的な、ファイル単位の移行(推奨)。リーフファイルから開始し、エントリファイルで終了する
- 寛容モードでの初期設定:共存を許可するために allowJs を有効化し、noImplicitAny を無効化し、strict を無効化
- 一括変換:.js → .ts—まず型注釈を追加し(フォールバックとして
anyを使用)、その後型を精緻化する - JSDocの型アノテーションは、ファイル拡張子を変更しないという妥協案であり、大規模プロジェクトでの段階的な移行に最適です
- よくある落とし穴:暗黙の
any爆発、module.exportsの競合、型指定のないサードパーティ製ライブラリ、およびthis型の欠落 - 厳格なオプションを段階的に有効にしていく――オプションを1つ有効にするたびに、すべてのエラーが修正される
📝 練習問題
- 基本演習(難易度 ⭐):単純な JavaScript ユーティリティファイル(3~5 つの関数)を TypeScript に移植してください。各関数のパラメータと戻り値に型注釈を追加し、正常にコンパイルされることを確認してください。
- 上級者向け演習(難易度:⭐⭐):JS/TSが混在するプロジェクトに対応できるよう
tsconfig.jsonを設定する――allowJsを有効にし、JSDocを使用してJSファイルに型情報を追加し、それをTSファイルでインポートして使用する。 - 課題(難易度:⭐⭐⭐):Expressプロジェクトの移行をシミュレートする――
tsconfigを設定し、ExpressのRequest/Responseに型を追加し、req.bodyの型安全性を確保し(bodyインターフェースを定義)、req.paramsの型を処理する。



