typescript-migration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TypeScript 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
    any
    usage
  • 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 TypeScript

Phase 1: Project Setup

阶段1:项目配置

1.1 Install TypeScript

1.1 安装TypeScript

bash
npm install --save-dev typescript @types/node
For framework-specific types:
bash
undefined
bash
npm install --save-dev typescript @types/node
框架专属类型安装:
bash
undefined

React

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

无需额外安装

undefined
undefined

1.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:
  • allowJs: true
    — allows mixed JS/TS
  • checkJs: false
    — don't type-check JS files yet
  • strict: false
    — start permissive
  • noImplicitAny: false
    — allow implicit
    any
    initially
初始使用宽松配置,后续逐步收紧:
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 Update Build Tools

1.3 更新构建工具

Most modern tools support TS out of the box:
bash
undefined
大多数现代工具默认支持TS:
bash
undefined

Vite — 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
undefined
npm install --save-dev ts-loader
undefined

Phase 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
undefined
bash
undefined

Rename

重命名

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
undefined
npx tsc --noEmit
undefined

2.3 Typing Strategy Per File

2.3 单文件类型定义策略

For each renamed file:
  1. Add explicit return types to exported functions
  2. Add parameter types to exported functions
  3. Add interface/type for complex objects
  4. Use
    unknown
    instead of
    any
    where possible
  5. 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}`
}
对每个重命名的文件:
  1. 为导出的函数添加显式返回类型
  2. 为导出的函数添加参数类型
  3. 为复杂对象添加interface/type定义
  4. 尽可能使用
    unknown
    代替
    any
  5. 初始阶段保持内部函数类型宽松 — 后续再收紧
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 | HPKMovementMessage

Phase 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": true
json
// 步骤1 — 捕获null/undefined相关bug(收益最高)
"strictNullChecks": true

// 步骤2 — 捕获缺失的类型定义
"noImplicitAny": true

// 步骤3 — 捕获遗漏的返回值
"noImplicitReturns": true

// 步骤4 — 捕获switch语句落空情况
"noFallthroughCasesInSwitch": true

// 步骤5 — 完全严格模式(启用所有严格规则)
"strict": true

Dealing with
strictNullChecks
Errors

处理
strictNullChecks
报错

Most 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

typescript
// 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
.ts
and all strict checks pass individually:
json
{
  "compilerOptions": {
    "strict": true,
    "allowJs": false,
    "checkJs": false
  }
}
Remove
allowJs
since all files are now TypeScript.
当所有文件都已改为
.ts
格式,且所有严格检查都单独通过后:
json
{
  "compilerOptions": {
    "strict": true,
    "allowJs": false,
    "checkJs": false
  }
}
删除
allowJs
配置,因为所有文件现在都是TypeScript。

Common 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

常见陷阱

PitfallWhy It's BadFix
as any
everywhere
Defeats the purpose of TypeScriptUse
unknown
+ type guards
Rewriting code during migrationIntroduces bugs, blocks progressMigrate types only, refactor later
Starting with strict modeToo many errors, team gives upStart permissive, tighten gradually
Migrating test files firstTests don't need strict typesMigrate source first, tests last
Giant PR with 50 filesUnreviewable, merge conflictsOne file or module per PR
Ignoring
@types
packages
Missing types for dependenciesInstall
@types/*
as you go
陷阱负面影响解决方案
到处使用
as any
完全违背了使用TypeScript的意义使用
unknown
+ 类型守卫
迁移过程中重写代码会引入bug,阻碍迁移进度仅迁移类型,后续再重构
一开始就启用严格模式错误过多,导致团队放弃从宽松配置开始,逐步收紧
优先迁移测试文件测试不需要严格类型优先迁移源码,最后迁移测试
包含50个文件的巨型PR无法评审,容易出现合并冲突每个PR仅迁移一个文件或模块
忽略
@types
缺少依赖的类型定义按需安装
@types/*