TypeScript: TypeScript JS 项目迁移到 TS

最后更新:2026-08-26

把现有 JavaScript 项目迁移到 TypeScript 是最常见也最实际的场景——本课讲解如何安全、渐进地完成迁移。

1. 迁移策略概述

(1) 三种迁移策略

策略 速度 风险 适用场景
一次性全量迁移 小项目(< 20 文件)
增量逐文件迁移 中大项目(推荐)
JSDoc 渐进迁移 极低 大型/关键项目
📌 推荐:增量逐文件迁移。 先让 TS 编译器接受 JS 文件,然后逐个把 .js 改为 .ts,每改一个文件就确保编译通过。

(2) 迁移步骤总览

TEXT 📖 仅展示
1. 初始化 tsconfig.json(宽松模式)
2. 安装 @types 包
3. 开启 allowJs——TS 和 JS 共存
4. 逐文件 .js → .ts(从叶子文件开始)
5. 逐步开启严格选项
6. 最终开启 strict

2. 第一步:初始化配置

(1) 生成 tsconfig.json

BASH
tsc --init

(2) 关键初始配置——宽松模式

JSON
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "allowJs": true,
    "checkJs": false,
    "noImplicitAny": false,
    "strict": false,
    "outDir": "./dist",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
💡 关键: 迁移初期不开 strict 和 noImplicitAny——先让项目能编译通过,再逐步收紧。

(3) 安装类型声明

BASH
# 安装项目依赖的类型声明
npm install @types/node @types/express @types/lodash --save-dev

3. 第二步:allowJs 让 JS 和 TS 共存

(1) 开启 allowJs

JSON
{
  "compilerOptions": {
    "allowJs": true
  }
}

allowJs 让 TypeScript 编译器接受 .js 文件——项目可以同时包含 JS 和 TS 文件,互不影响。

(2) 导入兼容

TYPESCRIPT
// main.ts —— 可以导入 .js 文件
import { helper } from "./utils";  // utils.js 存在即可

// utils.js —— JS 文件,无类型注解
export function helper(value) {
  return value.toString();
}

(3) checkJs——可选的 JS 类型检查

JSON
{
  "compilerOptions": {
    "checkJs": true
  }
}

开启 checkJs 后,TypeScript 也会检查 .js 文件的类型错误(基于 JSDoc 注释和推断)。迁移初期建议关闭——等 JS 文件逐步改为 TS 后再开。


4. 第三步:逐文件迁移

(1) 迁移顺序

从依赖最少的文件开始——叶子文件(不导入其他项目文件的工具函数、常量等):

TEXT 📖 仅展示
迁移顺序建议:
1. 常量文件(config.js → config.ts)
2. 工具函数(utils.js → utils.ts)
3. 类型定义(types.js → types.ts)
4. 数据模型(models.js → models.ts)
5. 服务层(services.js → services.ts)
6. 控制器/路由(controllers.js → controllers.ts)
7. 入口文件(index.js → index.ts)

(2) 单个文件的迁移步骤

TYPESCRIPT
// ── 迁移前:utils.js ──
export function formatPrice(price, currency) {
  return currency + price.toFixed(2);
}

export function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}
TYPESCRIPT
// ── 迁移后:utils.ts ──
export function formatPrice(price: number, currency: string = "¥"): string {
  return currency + price.toFixed(2);
}

export function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max);
}

(3) 迁移技巧——先用 any 后收紧

TYPESCRIPT
// 第一步:加上类型注解,不确定的用 any
function process(data: any, options: any): any {
  return data.filter(item => item.active);
}

// 第二步:逐步替换 any 为具体类型
interface DataItem {
  id: number;
  name: string;
  active: boolean;
}

interface Options {
  limit?: number;
  sort?: "asc" | "desc";
}

function process(data: DataItem[], options: Options = {}): DataItem[] {
  let result = data.filter(item => item.active);
  if (options.sort === "desc") result.reverse();
  if (options.limit) result = result.slice(0, options.limit);
  return result;
}

▶ 示例:迁移一个 Express 路由文件

TYPESCRIPT
// ── 迁移前:users.js ──
const express = require("express");
const router = express.Router();

router.get("/", async (req, res) => {
  const users = await User.findAll();
  res.json(users);
});

router.post("/", async (req, res) => {
  const { name, email } = req.body;
  const user = await User.create({ name, email });
  res.status(201).json(user);
});

module.exports = router;
▶ 试一试
TYPESCRIPT
// ── 迁移后:users.ts ──
import { Router, Request, Response } from "express";

const router = Router();

interface CreateUserBody {
  name: string;
  email: string;
}

router.get("/", async (_req: Request, res: Response) => {
  const users = await User.findAll();
  res.json(users);
});

router.post("/", async (req: Request<{}, {}, CreateUserBody>, res: Response) => {
  const { name, email } = req.body;
  const user = await User.create({ name, email });
  res.status(201).json(user);
});

export default router;

5. JSDoc 类型注解

不想改文件扩展名时,可以用 JSDoc 给 JS 文件添加类型:

(1) 基本类型注解

JAVASCRIPT
// utils.js —— 用 JSDoc 添加类型
/**
 * 格式化价格
 * @param {number} price - 价格
 * @param {string} [currency="¥"] - 货币符号
 * @returns {string} 格式化后的价格
 */
export function formatPrice(price, currency = "¥") {
  return currency + price.toFixed(2);
}

/**
 * @typedef {Object} User
 * @property {number} id
 * @property {string} name
 * @property {string} email
 */

/**
 * 查找用户
 * @param {number} id
 * @returns {Promise<User>}
 */
export async function findUser(id) {
  // ...
}

(2) 在 TS 文件中引用 JSDoc 类型

TYPESCRIPT
// main.ts
import { findUser } from "./utils";  // utils.js 有 JSDoc 类型

let user = await findUser(1);  // ✅ 类型推断正确(来自 JSDoc)

(3) 常用 JSDoc 类型标签

标签 用途 示例
@type 变量类型 @type {string}
@param 参数类型 @param {number} x
@returns 返回值类型 @returns {string}
@typedef 定义类型 @typedef {Object} User
@property 对象属性 @property {string} name
@template 泛型参数 @template T
@callback 回调类型 @callback Handler

6. 常见迁移陷阱

(1) 陷阱一:隐式 any 爆炸

TYPESCRIPT
// 迁移后大量隐式 any——不要急着开 noImplicitAny
function process(data) {  // data 隐式 any
  return data.map(item => item.name);  // item 也是 any
}

解决: 先让项目编译通过,再逐个标注类型,最后开 noImplicitAny。

(2) 陷阱二:module.exports 和 import 冲突

TYPESCRIPT
// JS 文件用 module.exports
module.exports = function() { /* ... */ };

// TS 导入需要 esModuleInterop
import fn from "./legacy";  // 需要 esModuleInterop: true

(3) 陷阱三:第三方库无类型

TYPESCRIPT
import untypedLib from "untyped-lib";  // ❌ 找不到声明文件

// 临时解决——创建 shim.d.ts
declare module "untyped-lib" {
  const lib: any;
  export default lib;
}

(4) 陷阱四:this 类型丢失

TYPESCRIPT
// JS 中 this 动态绑定
const obj = {
  name: "Charlie",
  greet() {
    console.log(this.name);  // ✅ JS 中 OK
  }
};

// TS 中 this 需要标注
const obj2 = {
  name: "Charlie",
  greet(this: { name: string }) {
    console.log(this.name);  // ✅ TS 中需要 this 参数
  }
};

❓ 常见问题

Q 迁移一个大项目需要多久?
A 取决于代码量和团队。中小项目(10,000 rows of以内)约1-2周。大型项目(100,000 rows of+)可能需要1-3个月的增量迁移。关键是不要一次性全改——逐文件迁移,每步都确保编译通过。
Q JSDoc 类型注解和 TS 类型注解哪个好?
A TS 类型注解更好——语法更简洁、更强大、编辑器支持更好。JSDoc 是"不改扩展名"的妥协方案,适合无法修改文件名的场景。最终目标还是把 JS 文件改为 TS。
Q 迁移过程中 CI 怎么办?
A CI 中加 tsc --noEmit 做类型检查(不输出文件)。迁移初期允许 --noImplicitAny false,确保 CI 不因类型问题失败。逐步收紧后,CI 的类型检查也越来越严格。
Q ts-ignore 和 ts-expect-error 该用哪个?
A 优先 @ts-expect-error——它如果下一行没有类型错误会报错(防止修复后忘记删除注释)。@ts-ignore 无条件忽略,可能掩盖已修复的错误。两者都是临时方案,最终应该修复类型问题。

📖 小节

📝 作业

  1. 基础题(难度⭐):把一个简单的 JS 工具文件(3-5个函数)迁移为 TS——给每个函数添加参数和返回值类型注解,确保编译通过。
  2. 进阶题(难度⭐⭐):配置 tsconfig.json 支持混合 JS/TS 项目——allowJs 开启,用 JSDoc 给一个 JS 文件添加类型,在 TS 文件中导入并使用。
  3. 挑战题(难度⭐⭐⭐):模拟迁移一个 Express 项目——配置 tsconfig,为 Express Request/Response 添加类型,处理 req.body 的类型安全(定义 body 接口),处理 req.params 的类型。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏