简介:为什么将遗留代码重构为 TypeScript 是必要的
处理遗留代码是开发者最常见也最具挑战性的任务之一。这类代码通常用无严格类型的纯 JavaScript 编写,包含复杂的依赖关系且缺乏模块化结构。迁移到 TypeScript 不仅增加了类型 — 更是代码质量的根本性提升,能够减少 bug 数量、简化重构流程,并加快新开发者的上手速度。
在本文中,我们将逐步解析将遗留代码重构为 TypeScript 的过程。您将学习如何安全地引入类型、拆分巨型函数以及测试变更。
第一步:遗留代码的分析与准备
在编写代码之前,必须了解当前的架构。没有这一步骤,重构将陷入混乱。
创建依赖关系图
从分析文件和模块入手。确定:
- 哪些函数是"纯函数",哪些有副作用。
- 全局变量和状态位于何处。
- 哪些代码段存在重复。
为关键部分编写测试
没有测试的重构无异于玩俄罗斯轮盘赌。至少为核心函数编写单元测试。以下是一个 TypeScript 测试示例:
// 原始遗留代码 (JavaScript)function calculatePrice(items) { let total = 0; for (let i = 0; i < items.length; i++) { total += items[i].price * items[i].quantity; } return total;}
// TypeScript 测试 (Jest)describe('calculatePrice', () => { it('应该正确计算总价', () => { const items = [ { price: 10, quantity: 2 }, { price: 5, quantity: 3 } ]; expect(calculatePrice(items)).toBe(35); });});如果测试通过,就可以开始重构了。
第二步:逐步引入严格类型
不要试图一次性用类型覆盖所有内容。采用"自底向上"策略:从最简单、最独立的函数开始。
为简单函数添加类型
为参数和返回值添加类型。示例:
// 之前 (JavaScript)function greet(name) { return "Hello, " + name;}
// 之后 (TypeScript)function greet(name: string): string { return `Hello, ${name}`;}处理对象与接口
为复杂数据结构创建接口。这能让代码具备自文档化能力:
// 为订单项定义接口interface OrderItem { id: number; name: string; price: number; quantity: number;}
// 带类型参数的函数function getTotalPrice(items: OrderItem[]): number { return items.reduce((sum, item) => sum + item.price * item.quantity, 0);}仅将 `any` 作为临时解决方案
any 类型是严格类型的敌人。如果你不确定具体类型,请使用 unknown 并在运行时进行检查:
function safeParse(data: unknown): string { if (typeof data === 'string') { return data.trim(); } return '';}第三步:拆分巨型函数与模块
遗留代码通常包含"上帝函数",它们一次性完成所有操作。将这些函数拆解为小型、可复用的模块。
提取纯逻辑
示例:一个同时验证邮箱并发送请求的函数。我们来拆分它:
// 之前 (巨型函数)function processUser(email) { if (!email || !email.includes('@')) { throw new Error('无效的邮箱'); } // ... 发送请求 console.log('正在发送请求...');}
// 之后 (关注点分离)function validateEmail(email: string): boolean { return email