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. 04为 API 和共享接口添加类型
  5. 05逐步启用严格编译检查

适用场景

  1. 01为现有 JavaScript 项目引入 TypeScript
  2. 02逐步转换代码文件
  3. 03减少旧代码中的 `any`
  4. 04为混合 JS/TS 项目启用严格模式

安装

复制下面的命令,在终端中运行。

$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 每次重命名一个文件
# 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. 尽可能使用 unknown 代替 any
  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:收紧编译器检查

逐项启用更严格的检查。每次更改后,先修复所有错误,再启用下一项。

推荐顺序
// 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)
启用严格模式之前
[ ] 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 + 类型守卫
迁移期间重写代码会引入错误并阻碍进度只迁移类型,之后再重构
一开始就启用严格模式错误太多,团队会放弃从宽松设置开始,逐步收紧
先迁移测试文件测试不需要严格类型先迁移源代码,最后迁移测试
一个包含 50 个文件的超大 PR无法审查,容易产生合并冲突每个 PR 迁移一个文件或模块
忽略 @types 包依赖项缺少类型按需安装 @types/*

评论

支持 Markdown