mcpField
T
typescript-migration

typescript-migration

github.com/dedalus-erp-pas開発117 回インストール
リポジトリ

typescript-migration について

既存のJavaScriptコードベースを一括で書き換えず、段階的にTypeScriptへ移行するためのスキルです。寛容な初期設定、ファイル単位の変換、公開境界の型付け、厳格なチェックの段階的な導入を扱います。

主な機能

  1. 01段階的なJavaScriptからTypeScriptへの移行
  2. 02寛容な混在コードベースの設定
  3. 03依存の葉から始めるファイル変換
  4. 04APIと共有インターフェースの型付け
  5. 05コンパイラの厳格化を段階的に実施

活用シーン

  1. 01既存のJavaScriptプロジェクトへのTypeScript導入
  2. 02ファイル単位でのコード変換
  3. 03レガシーコードの`any`削減
  4. 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 ファイルごとの型付け戦略

名前を変更したファイルごとに、次の作業を行います。

  1. エクスポートされた関数に明示的な戻り値の型を追加する
  2. エクスポートされた関数に引数の型を追加する
  3. 複雑なオブジェクトにインターフェースまたは型を追加する
  4. 可能な場合は any の代わりに unknown を使用する
  5. 最初は内部関数の型付けを緩く保ち、後から厳しくする
// 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/* をインストールします

コメント