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 无条件忽略,可能掩盖已修复的错误。两者都是临时方案,最终应该修复类型问题。📖 小节
- 迁移策略:增量逐文件迁移(推荐),从叶子文件开始,入口文件最后
- 初始配置宽松模式:allowJs 开启共存,noImplicitAny 关闭,strict 关闭
- 单文件迁移:.js → .ts,先加类型注解(any 兜底),后收紧类型
- JSDoc 类型注解是不改扩展名的妥协方案——适合大项目渐进迁移
- 常见陷阱:隐式 any 爆炸、module.exports 冲突、第三方库无类型、this 类型丢失
- 逐步开启严格选项——每开一个选项就修复所有报错
📝 作业
- 基础题(难度⭐):把一个简单的 JS 工具文件(3-5个函数)迁移为 TS——给每个函数添加参数和返回值类型注解,确保编译通过。
- 进阶题(难度⭐⭐):配置 tsconfig.json 支持混合 JS/TS 项目——allowJs 开启,用 JSDoc 给一个 JS 文件添加类型,在 TS 文件中导入并使用。
- 挑战题(难度⭐⭐⭐):模拟迁移一个 Express 项目——配置 tsconfig,为 Express Request/Response 添加类型,处理
req.body的类型安全(定义 body 接口),处理req.params的类型。