typescript-migration について
既存のJavaScriptコードベースを一括で書き換えず、段階的にTypeScriptへ移行するためのスキルです。寛容な初期設定、ファイル単位の変換、公開境界の型付け、厳格なチェックの段階的な導入を扱います。
主な機能
- 01段階的なJavaScriptからTypeScriptへの移行
- 02寛容な混在コードベースの設定
- 03依存の葉から始めるファイル変換
- 04APIと共有インターフェースの型付け
- 05コンパイラの厳格化を段階的に実施
活用シーン
- 01既存のJavaScriptプロジェクトへのTypeScript導入
- 02ファイル単位でのコード変換
- 03レガシーコードの`any`削減
- 04JS/TS混在プロジェクトのstrict mode準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dedalus-erp-pas/ foundation-skills --skill typescript-migrationインストール後、Claude Code で/typescript-migrationを入力して読み込まれたか確認します。
詳細
TypeScript 移行ガイド
JavaScript コードベースを TypeScript に移行するための段階的な戦略です。段階的で、既存の作業を妨げない移行が必要なレガシープロジェクト向けに設計されています。
このスキルを使う場面
ユーザーが次のことを希望している場合に有効にします。
- JS プロジェクト(またはその一部)を TypeScript に変換したい
- 既存の JavaScript プロジェクトに TypeScript を追加したい
- レガシーコードへの型付けや、
anyの使用を減らす方法について質問している - 型安全性を段階的に向上させたい
- JS/TS 混在コードベース向けに tsconfig を設定している
基本原則:段階的な移行
一度にすべてを書き直さないでください。 依存関係ツリーの末端から始めて、ファイルごとに移行します。どの中間段階でも、コードベースが動作する状態を保つ必要があります。
Phase 1: Setup → tsconfig + tooling, zero code changes
Phase 2: Rename → .js → .ts for leaf files, fix type errors
Phase 3: Type boundaries → Add types to public APIs and shared interfaces
Phase 4: Deepen → Enable stricter checks, eliminate `any`
Phase 5: Strict mode → Full strict TypeScript
フェーズ 1:プロジェクトのセットアップ
1.1 TypeScript をインストールする
npm install --save-dev typescript @types/node
フレームワーク固有の型については、以下を参照してください。
# React
npm install --save-dev @types/react @types/react-dom
# Express
npm install --save-dev @types/express
# Vue (types included in vue package)
# No extra install needed
1.2 tsconfig.json を作成する
最初は制約を緩くし、後から厳しくします。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"allowJs": true,
"checkJs": false,
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.*"]
}
移行時の主な設定:
allowJs: true— JS と TS の混在を許可しますcheckJs: false— まだ JS ファイルの型チェックを行いませんstrict: false— 制約を緩くして開始しますnoImplicitAny: false— 最初は暗黙のanyを許可します
1.3 ビルドツールを更新する
最新のツールの多くは、標準で TS をサポートしています。
# Vite — zero config needed
# Next.js — zero config, just rename files
# Webpack — add ts-loader or use babel
npm install --save-dev ts-loader
フェーズ 2:名前を変更して修正する(末端から開始)
2.1 移行の順序を特定する
内部インポートがないファイル(リーフノード)から始め、順に上位へ進めます。
Level 0 (first): utils/formatDate.js → no imports from src/
Level 0 (first): constants/index.js → no imports from src/
Level 1: services/dateService.js → imports from utils/
Level 2: api/patientApi.js → imports from services/
Level 3 (last): pages/PatientList.jsx → imports from everything
2.2 ファイルを 1 つずつ名前変更する
# Rename
mv src/utils/formatDate.js src/utils/formatDate.ts
# Fix type errors
# Run build/IDE to see errors
npx tsc --noEmit
2.3 ファイルごとの型付け戦略
名前を変更したファイルごとに、次の作業を行います。
- エクスポートされた関数に明示的な戻り値の型を追加する
- エクスポートされた関数に引数の型を追加する
- 複雑なオブジェクトにインターフェースまたは型を追加する
- 可能な場合は
anyの代わりにunknownを使用する - 最初は内部関数の型付けを緩く保ち、後から厳しくする
// BEFORE (JavaScript)
export function formatPatientName(patient) {
return `${patient.lastName} ${patient.firstName}`
}
// AFTER (TypeScript — Phase 2)
interface Patient {
lastName: string
firstName: string
}
export function formatPatientName(patient: Patient): string {
return `${patient.lastName} ${patient.firstName}`
}
フェーズ 3:型の境界
パブリック API の範囲、つまりエクスポート、関数シグネチャ、共有型の型付けに重点を置きます。
3.1 共有型ファイルを作成する
// src/types/patient.ts
export interface Patient {
id: string
lastName: string
firstName: string
birthDate: string // ISO 8601
sex: 'M' | 'F' | 'U'
ipp?: string // Internal Patient ID (optional)
}
export interface PatientSearchParams {
query?: string
unit?: string
page?: number
limit?: number
}
export type PatientCreateInput = Omit<Patient, 'id'>
3.2 API レスポンスに型を付ける
// src/types/api.ts
export interface ApiResponse<T> {
success: boolean
data: T
error?: ApiError
}
export interface ApiError {
code: string
message: string
details?: Record<string, string[]>
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
3.3 イベント/メッセージのペイロードに型を付ける
医療メッセージの処理では特に重要です。
// src/types/hpk.ts
export type HPKMessageType = 'ID' | 'MV' | 'CV' | 'PR' | 'FO'
export type HPKMode = 'C' | 'M' | 'D'
export interface HPKMessage {
type: HPKMessageType
code: string
mode: HPKMode
sender: string
timestamp: string
userId: string
fields: string[]
}
// Discriminated union for type-safe message handling
export type HPKIdentityMessage = HPKMessage & {
type: 'ID'
patient: {
id: string
lastName: string
firstName: string
birthDate: string
sex: 'M' | 'F'
}
}
export type HPKMovementMessage = HPKMessage & {
type: 'MV'
visit: {
id: string
unit: string
bed?: string
}
}
export type TypedHPKMessage = HPKIdentityMessage | HPKMovementMessage
フェーズ 4:コンパイラーのチェックを厳しくする
厳格なチェックを 1 つずつ有効にします。設定を変更するたびに、次のチェックを有効にする前にすべてのエラーを修正します。
推奨される順序
// Step 1 — catch null/undefined bugs (highest value)
"strictNullChecks": true
// Step 2 — catch missing types
"noImplicitAny": true
// Step 3 — catch forgotten returns
"noImplicitReturns": true
// Step 4 — catch switch fallthrough
"noFallthroughCasesInSwitch": true
// Step 5 — full strict mode (enables all strict options)
"strict": true
strictNullChecks のエラーへの対処
よくあるパターンは次のとおりです。
// Problem: Object is possibly 'undefined'
const patient = patients.find(p => p.id === id)
patient.name // Error!
// Fix 1: Guard clause (preferred)
if (!patient) {
throw new Error(`Patient ${id} not found`)
}
patient.name // OK — TypeScript narrows the type
// Fix 2: Optional chaining (when absence is expected)
const name = patient?.name ?? 'Unknown'
// Fix 3: Non-null assertion (LAST RESORT — avoid if possible)
patient!.name // Suppresses error, but defeats the purpose
any をなくす
// STEP 1: Replace `any` with `unknown`
function parseMessage(raw: unknown): HPKMessage {
if (typeof raw !== 'string') {
throw new Error('Expected string input')
}
// Now TypeScript knows raw is a string
const parts = raw.split('|')
// ...
}
// STEP 2: Use type guards for runtime checking
function isPatient(data: unknown): data is Patient {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'lastName' in data
)
}
// STEP 3: Use Zod for validated parsing (recommended)
import { z } from 'zod'
const PatientSchema = z.object({
id: z.string(),
lastName: z.string(),
firstName: z.string(),
birthDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
sex: z.enum(['M', 'F', 'U']),
})
type Patient = z.infer<typeof PatientSchema>
function parsePatient(data: unknown): Patient {
return PatientSchema.parse(data) // Throws ZodError if invalid
}
フェーズ 5:厳格モード
すべてのファイルが .ts になり、すべての厳格なチェックに個別に合格したら、次のようにします。
{
"compilerOptions": {
"strict": true,
"allowJs": false,
"checkJs": false
}
}
すべてのファイルが TypeScript になったため、allowJs を削除します。
レガシーコードでよく使われるパターン
コールバックの多いコードに型を付ける
// Legacy pattern with callbacks
function fetchData(url: string, callback: (err: Error | null, data?: unknown) => void): void
// Modern replacement
async function fetchData(url: string): Promise<unknown>
動的オブジェクトに型を付ける
// When the shape varies at runtime (e.g., config, feature flags)
type Config = Record<string, string | number | boolean>
// When you know some keys but not all
interface AppConfig {
apiUrl: string
debug: boolean
[key: string]: unknown // Allow additional unknown keys
}
型定義のないサードパーティライブラリに型を付ける
// Create src/types/untyped-lib.d.ts
declare module 'legacy-hpk-decoder' {
export function decode(message: string): Record<string, string>
export function encode(fields: Record<string, string>): string
}
Express ミドルウェアに型を付ける
import { Request, Response, NextFunction } from 'express'
interface AuthenticatedRequest extends Request {
user: {
id: string
roles: string[]
}
}
function requireAuth(req: Request, res: Response, next: NextFunction): void {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
res.status(401).json({ error: 'Unauthorized' })
return
}
// Attach user to request
;(req as AuthenticatedRequest).user = verifyToken(token)
next()
}
移行チェックリスト
開始前
[ ] Team aligned on migration (not a solo effort)
[ ] Build pipeline supports .ts files
[ ] IDE configured for TypeScript (tsserver)
[ ] tsconfig.json created with permissive settings
[ ] @types packages installed for dependencies
ファイルごと
[ ] File renamed .js → .ts (or .jsx → .tsx)
[ ] Exported functions have explicit parameter and return types
[ ] Complex objects have interfaces/types defined
[ ] No new `any` introduced (use `unknown` + type guards)
[ ] Tests still pass after conversion
[ ] No `// @ts-ignore` or `// @ts-expect-error` (unless temporary, with TODO)
strict を有効にする前
[ ] All files are .ts/.tsx
[ ] strictNullChecks enabled and all errors fixed
[ ] noImplicitAny enabled and all errors fixed
[ ] No remaining `any` in public APIs
[ ] Shared types defined in types/ directory
[ ] Type coverage > 90%
よくある落とし穴
| 落とし穴 | 問題となる理由 | 対処法 |
|---|---|---|
どこでも as any を使う | TypeScript の目的が損なわれます | unknown と型ガードを使用します |
| 移行中にコードを書き直す | バグが入り、進捗が妨げられます | 型だけを移行し、リファクタリングは後で行います |
| strict モードから始める | エラーが多すぎて、チームが諦めてしまいます | 制約を緩くして開始し、徐々に厳しくします |
| 最初にテストファイルを移行する | テストに厳密な型は必要ありません | 最初にソースを移行し、テストは最後にします |
| 50 ファイルを含む巨大な PR | レビューが困難で、マージの競合が発生します | PR ごとに 1 ファイルまたは 1 モジュールにします |
@types パッケージを無視する | 依存関係の型が不足します | 必要に応じて @types/* をインストールします |