typescript-migration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTypeScript Migration Guide
TypeScript迁移指南
Incremental strategy for migrating JavaScript codebases to TypeScript. Designed for legacy projects that need progressive, non-disruptive migration.
用于将JavaScript代码库迁移到TypeScript的渐进式策略,专为需要渐进式、无中断迁移的遗留项目设计。
When to Use This Skill
何时使用本指南
Activate when the user:
- Wants to convert a JS project (or parts of it) to TypeScript
- Needs to add TypeScript to an existing JavaScript project
- Asks about typing legacy code or reducing usage
any - Wants to improve type safety incrementally
- Is setting up tsconfig for a mixed JS/TS codebase
当用户出现以下需求时启用:
- 想要将JS项目(或其部分内容)转换为TypeScript
- 需要在现有JavaScript项目中添加TypeScript支持
- 询问如何为遗留代码添加类型或减少的使用
any - 想要逐步提升类型安全性
- 正在为JS/TS混合代码库配置tsconfig
Core Principle: Incremental Migration
核心原则:渐进式迁移
Never rewrite everything at once. Migrate file by file, starting from the leaves of the dependency tree. Every intermediate state must be a working codebase.
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永远不要一次性重写所有代码。 从依赖树的叶子节点开始,逐文件迁移。每一个中间状态都必须保证代码库可正常运行。
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 TypeScriptPhase 1: Project Setup
阶段1:项目配置
1.1 Install TypeScript
1.1 安装TypeScript
bash
npm install --save-dev typescript @types/nodeFor framework-specific types:
bash
undefinedbash
npm install --save-dev typescript @types/node框架专属类型安装:
bash
undefinedReact
React
npm install --save-dev @types/react @types/react-dom
npm install --save-dev @types/react @types/react-dom
Express
Express
npm install --save-dev @types/express
npm install --save-dev @types/express
Vue (types included in vue package)
Vue (类型已内置在vue包中)
No extra install needed
无需额外安装
undefinedundefined1.2 Create tsconfig.json
1.2 创建tsconfig.json
Start permissive, tighten later:
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.*"]
}Key settings for migration:
- — allows mixed JS/TS
allowJs: true - — don't type-check JS files yet
checkJs: false - — start permissive
strict: false - — allow implicit
noImplicitAny: falseinitiallyany
初始使用宽松配置,后续逐步收紧:
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.*"]
}迁移核心配置说明:
- — 允许JS/TS混合存在
allowJs: true - — 暂时不对JS文件做类型检查
checkJs: false - — 初始使用宽松配置
strict: false - — 初始阶段允许隐式
noImplicitAny: falseany
1.3 Update Build Tools
1.3 更新构建工具
Most modern tools support TS out of the box:
bash
undefined大多数现代工具默认支持TS:
bash
undefinedVite — zero config needed
Vite — 无需额外配置
Next.js — zero config, just rename files
Next.js — 无需额外配置,仅需重命名文件即可
Webpack — add ts-loader or use babel
Webpack — 添加ts-loader或使用babel
npm install --save-dev ts-loader
undefinednpm install --save-dev ts-loader
undefinedPhase 2: Rename and Fix (Leaf-First)
阶段2:重命名与修复(叶子节点优先)
2.1 Identify Migration Order
2.1 确定迁移顺序
Start from files with NO internal imports (leaf nodes), then work up:
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从无内部导入的文件(叶子节点)开始,逐步向上迁移:
Level 0 (最先迁移): utils/formatDate.js → 没有来自src/的导入
Level 0 (最先迁移): constants/index.js → 没有来自src/的导入
Level 1: services/dateService.js → 从utils/导入
Level 2: api/patientApi.js → 从services/导入
Level 3 (最后迁移): pages/PatientList.jsx → 从所有模块导入2.2 Rename One File at a Time
2.2 一次仅重命名一个文件
bash
undefinedbash
undefinedRename
重命名
mv src/utils/formatDate.js src/utils/formatDate.ts
mv src/utils/formatDate.js src/utils/formatDate.ts
Fix type errors
修复类型错误
Run build/IDE to see errors
运行构建/IDE查看错误
npx tsc --noEmit
undefinednpx tsc --noEmit
undefined2.3 Typing Strategy Per File
2.3 单文件类型定义策略
For each renamed file:
- Add explicit return types to exported functions
- Add parameter types to exported functions
- Add interface/type for complex objects
- Use instead of
unknownwhere possibleany - Keep internal functions loosely typed initially — tighten later
typescript
// 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}`
}对每个重命名的文件:
- 为导出的函数添加显式返回类型
- 为导出的函数添加参数类型
- 为复杂对象添加interface/type定义
- 尽可能使用代替
unknownany - 初始阶段保持内部函数类型宽松 — 后续再收紧
typescript
// 改造前 (JavaScript)
export function formatPatientName(patient) {
return `${patient.lastName} ${patient.firstName}`
}
// 改造后 (TypeScript — 阶段2)
interface Patient {
lastName: string
firstName: string
}
export function formatPatientName(patient: Patient): string {
return `${patient.lastName} ${patient.firstName}`
}Phase 3: Type Boundaries
阶段3:定义类型边界
Focus on typing the public API surface — exports, function signatures, shared types.
重点为公共API层添加类型:导出内容、函数签名、共享类型。
3.1 Create Shared Type Files
3.1 创建共享类型文件
typescript
// 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'>typescript
// src/types/patient.ts
export interface Patient {
id: string
lastName: string
firstName: string
birthDate: string // ISO 8601
sex: 'M' | 'F' | 'U'
ipp?: string // 内部患者ID (可选)
}
export interface PatientSearchParams {
query?: string
unit?: string
page?: number
limit?: number
}
export type PatientCreateInput = Omit<Patient, 'id'>3.2 Type API Responses
3.2 为API响应添加类型
typescript
// 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
}
}typescript
// 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 Type Event/Message Payloads
3.3 为事件/消息负载添加类型
Especially important for healthcare message processing:
typescript
// 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这对医疗消息处理场景尤其重要:
typescript
// 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[]
}
// 可辨识联合类型,实现类型安全的消息处理
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 | HPKMovementMessagePhase 4: Tighten the Compiler
阶段4:收紧编译器规则
Enable stricter checks one at a time. After each change, fix all errors before enabling the next.
逐个启用更严格的检查规则,每次修改后修复所有错误再启用下一项规则。
Recommended Order
推荐启用顺序
json
// 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": truejson
// 步骤1 — 捕获null/undefined相关bug(收益最高)
"strictNullChecks": true
// 步骤2 — 捕获缺失的类型定义
"noImplicitAny": true
// 步骤3 — 捕获遗漏的返回值
"noImplicitReturns": true
// 步骤4 — 捕获switch语句落空情况
"noFallthroughCasesInSwitch": true
// 步骤5 — 完全严格模式(启用所有严格规则)
"strict": trueDealing with strictNullChecks
Errors
strictNullChecks处理strictNullChecks
报错
strictNullChecksMost common patterns:
typescript
// 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最常见的处理模式:
typescript
// 问题:对象可能为 'undefined'
const patient = patients.find(p => p.id === id)
patient.name // 报错!
// 修复1:守卫子句(推荐)
if (!patient) {
throw new Error(`Patient ${id} not found`)
}
patient.name // 正常 — TypeScript会收窄类型
// 修复2:可选链(允许值不存在的场景)
const name = patient?.name ?? 'Unknown'
// 修复3:非空断言(最后选择 — 尽量避免)
patient!.name // 抑制报错,但失去类型检查意义Eliminating any
any消除any
anytypescript
// 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
}typescript
// 步骤1:用`unknown`替换`any`
function parseMessage(raw: unknown): HPKMessage {
if (typeof raw !== 'string') {
throw new Error('Expected string input')
}
// 现在TypeScript已知raw是字符串
const parts = raw.split('|')
// ...
}
// 步骤2:使用类型守卫做运行时检查
function isPatient(data: unknown): data is Patient {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'lastName' in data
)
}
// 步骤3:使用Zod做验证解析(推荐)
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) // 验证不通过会抛出ZodError
}Phase 5: Strict Mode
阶段5:严格模式
When all files are and all strict checks pass individually:
.tsjson
{
"compilerOptions": {
"strict": true,
"allowJs": false,
"checkJs": false
}
}Remove since all files are now TypeScript.
allowJs当所有文件都已改为格式,且所有严格检查都单独通过后:
.tsjson
{
"compilerOptions": {
"strict": true,
"allowJs": false,
"checkJs": false
}
}删除配置,因为所有文件现在都是TypeScript。
allowJsCommon Patterns for Legacy Code
遗留代码常见处理模式
Typing Callback-Heavy Code
回调密集型代码类型定义
typescript
// 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>typescript
// 传统回调模式
function fetchData(url: string, callback: (err: Error | null, data?: unknown) => void): void
// 现代替换方案
async function fetchData(url: string): Promise<unknown>Typing Dynamic Objects
动态对象类型定义
typescript
// 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
}typescript
// 运行时结构可变的场景(如配置、功能开关)
type Config = Record<string, string | number | boolean>
// 已知部分key但不完整的场景
interface AppConfig {
apiUrl: string
debug: boolean
[key: string]: unknown // 允许额外的未知key
}Typing Third-Party Libraries Without Types
无类型的第三方库类型定义
typescript
// 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
}typescript
// 创建 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
}Typing Express Middleware
Express中间件类型定义
typescript
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()
}typescript
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
}
// 向request挂载user信息
;(req as AuthenticatedRequest).user = verifyToken(token)
next()
}Migration Checklist
迁移检查清单
Before Starting
开始前
[ ] 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[ ] 团队就迁移达成共识(不是单人工作)
[ ] 构建流水线支持.ts文件
[ ] IDE已配置TypeScript(tsserver)
[ ] 已创建宽松配置的tsconfig.json
[ ] 已安装依赖对应的@types包Per File
单文件迁移完成标准
[ ] 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)[ ] 文件已从.js重命名为.ts(或.jsx重命名为.tsx)
[ ] 导出函数已有显式的参数和返回类型
[ ] 复杂对象已有对应的interface/type定义
[ ] 没有引入新的`any`(使用`unknown` + 类型守卫)
[ ] 转换后测试全部通过
[ ] 没有`// @ts-ignore`或`// @ts-expect-error`(临时使用需标注TODO)Before Enabling 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%[ ] 所有文件均为.ts/.tsx格式
[ ] 已启用strictNullChecks并修复所有错误
[ ] 已启用noImplicitAny并修复所有错误
[ ] 公共API中没有残留的`any`
[ ] 共享类型已定义在types/目录下
[ ] 类型覆盖率 > 90%Common Pitfalls
常见陷阱
| Pitfall | Why It's Bad | Fix |
|---|---|---|
| Defeats the purpose of TypeScript | Use |
| Rewriting code during migration | Introduces bugs, blocks progress | Migrate types only, refactor later |
| Starting with strict mode | Too many errors, team gives up | Start permissive, tighten gradually |
| Migrating test files first | Tests don't need strict types | Migrate source first, tests last |
| Giant PR with 50 files | Unreviewable, merge conflicts | One file or module per PR |
Ignoring | Missing types for dependencies | Install |
| 陷阱 | 负面影响 | 解决方案 |
|---|---|---|
到处使用 | 完全违背了使用TypeScript的意义 | 使用 |
| 迁移过程中重写代码 | 会引入bug,阻碍迁移进度 | 仅迁移类型,后续再重构 |
| 一开始就启用严格模式 | 错误过多,导致团队放弃 | 从宽松配置开始,逐步收紧 |
| 优先迁移测试文件 | 测试不需要严格类型 | 优先迁移源码,最后迁移测试 |
| 包含50个文件的巨型PR | 无法评审,容易出现合并冲突 | 每个PR仅迁移一个文件或模块 |
忽略 | 缺少依赖的类型定义 | 按需安装 |