Loading...
Loading...
Compare original and translation side by side
anyanyPhase 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: 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 TypeScriptnpm install --save-dev typescript @types/nodeundefinednpm install --save-dev typescript @types/nodeundefinedundefinedundefined{
"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: truecheckJs: falsestrict: falsenoImplicitAny: falseany{
"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: truecheckJs: falsestrict: falsenoImplicitAny: falseanyundefinedundefinedundefinedundefinedLevel 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 everythingLevel 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 → 从所有模块导入undefinedundefinedundefinedundefinedunknownany// 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}`
}unknownany// 改造前 (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}`
}// 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'>// 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'>// 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
}
}// 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
}
}// 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// 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// 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// 步骤1 — 捕获null/undefined相关bug(收益最高)
"strictNullChecks": true
// 步骤2 — 捕获缺失的类型定义
"noImplicitAny": true
// 步骤3 — 捕获遗漏的返回值
"noImplicitReturns": true
// 步骤4 — 捕获switch语句落空情况
"noFallthroughCasesInSwitch": true
// 步骤5 — 完全严格模式(启用所有严格规则)
"strict": truestrictNullChecksstrictNullChecks// 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// 问题:对象可能为 '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 // 抑制报错,但失去类型检查意义anyany// 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
}// 步骤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
}.ts{
"compilerOptions": {
"strict": true,
"allowJs": false,
"checkJs": false
}
}allowJs.ts{
"compilerOptions": {
"strict": true,
"allowJs": false,
"checkJs": false
}
}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>// 传统回调模式
function fetchData(url: string, callback: (err: Error | null, data?: unknown) => void): void
// 现代替换方案
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
}// 运行时结构可变的场景(如配置、功能开关)
type Config = Record<string, string | number | boolean>
// 已知部分key但不完整的场景
interface AppConfig {
apiUrl: string
debug: boolean
[key: string]: unknown // 允许额外的未知key
}// 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
}// 创建 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
}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()
}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()
}[ ] 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包[ ] 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)[ ] 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%| 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仅迁移一个文件或模块 |
忽略 | 缺少依赖的类型定义 | 按需安装 |