TypeScript 开发指南 / 23 - 迁移指南
迁移指南
迁移策略
渐进式迁移(推荐)
Phase 1: 准备阶段 Phase 2: 基础阶段 Phase 3: 深化阶段
├─ 安装 TypeScript ├─ 重命名文件 .js→.ts ├─ 启用 strict
├─ 创建 tsconfig ├─ 添加基本类型 ├─ 完善类型定义
├─ 配置 allowJs ├─ 修复明显错误 ├─ 使用高级类型
└─ 安装 @types └─ 保持功能正常 └─ 移除 any
迁移原则
- 不要一次性全部迁移——逐步进行,每次只处理几个文件
- 保持功能正常——每次迁移后确保项目可以正常运行
- 先处理叶子模块——从依赖最少的模块开始
- any 是过渡工具——允许临时使用 any,后续逐步消除
Phase 1: 准备阶段
安装 TypeScript
npm install -D typescript @types/node
创建 tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowJs": true,
"checkJs": false,
"outDir": "./dist",
"rootDir": "./src",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
关键配置
| 配置 | 说明 |
|---|
allowJs: true | 允许编译 .js 文件 |
checkJs: false | 暂时不检查 .js 文件 |
strict: false | 暂时关闭严格模式 |
安装 @types
# 常用 @types
npm install -D @types/react @types/react-dom # React
npm install -D @types/express # Express
npm install -D @types/lodash # Lodash
npm install -D @types/jest # Jest
# 检查是否有类型定义
# DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped
# 类型搜索: https://www.typescriptlang.org/dt/search
Phase 2: 逐步迁移
步骤 1: 重命名文件
# 将 .js 文件重命名为 .ts
mv src/utils.js src/utils.ts
mv src/helpers.js src/helpers.ts
# JSX 文件使用 .tsx
mv src/App.jsx src/App.tsx
步骤 2: 修复基础错误
// 重命名后的文件可能有类型错误
// 临时使用 any 修复明显的错误
// 之前
function greet(name) {
return `Hello, ${name}!`;
}
// 临时修复(允许 any)
function greet(name: any) {
return `Hello, ${name}!`;
}
// 最终版本
function greet(name: string): string {
return `Hello, ${name}!`;
}
步骤 3: 从叶子模块开始
依赖关系图:
┌─────────┐
│ app.ts │
└────┬────┘
│
┌──────────┼──────────┐
│ │ │
┌─────┴─────┐ ┌──┴──┐ ┌────┴────┐
│ auth.ts │ │ db │ │ utils.ts│
└─────┬─────┘ └──┬──┘ └─────────┘
│ │
┌─────┴─────┐ ┌──┴──┐
│ crypto.ts │ │config│ ← 从这里开始
└───────────┘ └─────┘
实用的迁移脚本
// scripts/rename-to-ts.ts
import fs from "fs";
import path from "path";
import glob from "glob";
const files = glob.sync("src/**/*.js");
files.forEach(file => {
const newPath = file.replace(/\.js$/, ".ts");
fs.renameSync(file, newPath);
console.log(`Renamed: ${file} → ${newPath}`);
});
Phase 3: 启用严格模式
逐步启用严格选项
// 第一步:启用 noImplicitAny
{
"compilerOptions": {
"noImplicitAny": true
}
}
// 第二步:启用 strictNullChecks
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
// 第三步:完全启用 strict
{
"compilerOptions": {
"strict": true
}
}
处理 strictNullChecks
// strictNullChecks 开启后的问题
const element = document.getElementById("app");
element.innerHTML = "Hello"; // ❌ element 可能为 null
// 解决方案 1: 空值检查
if (element) {
element.innerHTML = "Hello";
}
// 解决方案 2: 非空断言(谨慎使用)
element!.innerHTML = "Hello";
// 解决方案 3: 可选链
element?.innerHTML;
// 解决方案 4: 默认值
const el = element ?? document.createElement("div");
@types 类型定义
理解 @types
# 安装类型定义
npm install -D @types/lodash
# 使用
import _ from "lodash";
_.chunk([1, 2, 3, 4], 2); // 类型安全
类型定义文件位置
node_modules/
├── @types/
│ ├── lodash/
│ │ └── index.d.ts # Lodash 的类型定义
│ ├── react/
│ │ └── index.d.ts # React 的类型定义
│ └── node/
│ └── index.d.ts # Node.js 的类型定义
创建自定义类型定义
// types/some-untyped-lib.d.ts
declare module "some-untyped-lib" {
export function doSomething(input: string): number;
export interface Options {
verbose?: boolean;
timeout?: number;
}
export default function main(options?: Options): void;
}
// 使用
import doSomething from "some-untyped-lib";
doSomething("hello"); // 类型安全
全局类型扩展
// types/global.d.ts
declare global {
// 扩展 Window 接口
interface Window {
__APP_CONFIG__: {
apiUrl: string;
debug: boolean;
};
}
// 扩展 Express Request
namespace Express {
interface Request {
user?: {
id: number;
role: string;
};
}
}
}
export {};
常见迁移问题
问题 1: 第三方库没有类型
// 解决方案 1: 创建声明文件
// types/untyped-lib.d.ts
declare module "untyped-lib" {
const lib: any;
export default lib;
}
// 解决方案 2: 使用 @ts-ignore
// @ts-ignore
import something from "untyped-lib";
// 解决方案 3: 使用 unknown
const data: unknown = require("untyped-lib");
问题 2: 动态属性访问
// JavaScript 风格
const value = obj[key];
// TypeScript 解决方案
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 或使用类型断言
const value = (obj as Record<string, any>)[key];
问题 3: 隐式 any
// JavaScript(隐式 any)
function process(data) {
return data.map(item => item.name);
}
// TypeScript 修复
function process(data: any[]): string[] {
return data.map(item => item.name);
}
// 最佳方案
interface Item {
name: string;
}
function process(data: Item[]): string[] {
return data.map(item => item.name);
}
问题 4: 类的 this 上下文
// JavaScript
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this);
}
}
// TypeScript 解决方案
class Component {
handleClick = () => {
console.log(this);
};
}
迁移检查清单
## 迁移检查清单
### 准备阶段
- [ ] 安装 TypeScript
- [ ] 创建 tsconfig.json
- [ ] 配置 allowJs: true
- [ ] 安装需要的 @types
- [ ] 配置构建工具支持 .ts 文件
### 文件迁移
- [ ] 重命名 .js → .ts(或 .jsx → .tsx)
- [ ] 修复编译错误(临时使用 any)
- [ ] 确保项目可以正常运行
### 类型完善
- [ ] 移除 any 类型
- [ ] 添加函数参数和返回值类型
- [ ] 定义接口和类型别名
- [ ] 处理 null 和 undefined
### 严格模式
- [ ] 启用 noImplicitAny
- [ ] 启用 strictNullChecks
- [ ] 启用 strict
- [ ] 修复所有严格模式错误
### 最终检查
- [ ] 确保 `tsc --noEmit` 无错误
- [ ] 确保所有测试通过
- [ ] 更新 CI/CD 配置
- [ ] 更新文档
业务场景:大型项目迁移计划
Week 1-2: 准备阶段
├─ 安装 TypeScript 和 @types
├─ 创建 tsconfig.json
├─ 配置 CI 运行 tsc --noEmit
└─ 培训团队成员
Week 3-4: 核心模块迁移
├─ 迁移 utils/ 和 helpers/
├─ 迁移 types/ 和 constants/
└─ 迁移核心业务逻辑
Week 5-8: 业务模块迁移
├─ 迁移 services/ 和 api/
├─ 迁移 components/ 和 pages/
└─ 迁移 hooks/ 和 contexts/
Week 9-10: 收尾
├─ 启用 strict 模式
├─ 消除 any 类型
├─ 更新文档和测试
└─ 移除 allowJs
注意事项
- 渐进式迁移——不要试图一次性迁移所有代码
- any 是过渡工具——允许临时使用,但要有计划地消除
- 先处理叶子模块——从依赖最少的模块开始
- 保持 CI 运行——确保每次迁移后项目仍然正常
- 团队培训——确保所有团队成员了解 TypeScript 基础
扩展阅读