关于 typescript-migration
帮助旧版 JavaScript 代码库逐步迁移到 TypeScript 的技能,避免一次性重写。内容涵盖宽松配置、逐文件转换、公共接口类型定义,以及逐步启用更严格的检查。
主要功能
- 01渐进式 JavaScript 转 TypeScript
- 02宽松的混合代码库配置
- 03从依赖叶节点开始逐文件转换
- 04为 API 和共享接口添加类型
- 05逐步启用严格编译检查
适用场景
- 01为现有 JavaScript 项目引入 TypeScript
- 02逐步转换代码文件
- 03减少旧代码中的 `any`
- 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 每个文件的类型策略
对每个重命名后的文件:
- 为导出的函数添加显式返回类型
- 为导出的函数添加参数类型
- 为复杂对象添加接口或类型
- 尽可能使用
unknown代替any - 初期让内部函数的类型保持宽松——之后再收紧
// 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