强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

23 - 迁移指南

迁移指南

迁移策略

渐进式迁移(推荐)

Phase 1: 准备阶段    Phase 2: 基础阶段    Phase 3: 深化阶段
├─ 安装 TypeScript   ├─ 重命名文件 .js→.ts ├─ 启用 strict
├─ 创建 tsconfig     ├─ 添加基本类型       ├─ 完善类型定义
├─ 配置 allowJs      ├─ 修复明显错误       ├─ 使用高级类型
└─ 安装 @types       └─ 保持功能正常       └─ 移除 any

迁移原则

  1. 不要一次性全部迁移——逐步进行,每次只处理几个文件
  2. 保持功能正常——每次迁移后确保项目可以正常运行
  3. 先处理叶子模块——从依赖最少的模块开始
  4. 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

注意事项

  1. 渐进式迁移——不要试图一次性迁移所有代码
  2. any 是过渡工具——允许临时使用,但要有计划地消除
  3. 先处理叶子模块——从依赖最少的模块开始
  4. 保持 CI 运行——确保每次迁移后项目仍然正常
  5. 团队培训——确保所有团队成员了解 TypeScript 基础

扩展阅读