tsc、Babel、esbuild 编译 TypeScript 有什么区别?isolatedModules 是什么?
一句话回答
tsc 是官方编译器,会读取整个项目的类型信息,做类型检查,再输出 JS 和 .d.ts。Babel、esbuild、swc 这类工具只做转译:一次处理一个文件,删掉类型、转换语法,不做类型检查,所以快得多,但要另外跑 tsc --noEmit 检查类型。单文件转译看不到其他文件,有些写法处理不了,比如跨文件的 const enum、没写 export type 的类型重导出。isolatedModules 就是让 tsc 把这些写法提前报出来;verbatimModuleSyntax 更进一步,要求类型导入导出都显式写 type。常见组合是:转译交给构建工具,tsc 只负责类型检查。
详细解析
两类工具的区别
| tsc | Babel / esbuild / swc | |
|---|---|---|
| 类型检查 | 有 | 没有,类型错误照样输出 |
| 处理单位 | 整个项目,能跨文件分析类型 | 一次一个文件 |
输出 .d.ts |
能 | 一般不能 |
| 速度 | 较慢,要建立完整的类型信息 | 快,只做语法层面的删除和转换 |
| 典型位置 | tsc --noEmit 做检查;库用它输出声明文件 |
Vite、webpack 的 loader、测试框架里转译代码 |
转译工具删除类型的方式很直接:遇到类型标注、interface、import type 就删掉,其余原样输出。它们不知道某个导入的名字到底是类型还是值,这就是下面那些限制的来源。Babel 的工作原理见 Babel 的原理,各构建工具的对比见 构建工具的区别。
TS 7.0 起官方编译器改用 Go 原生实现,类型检查快了很多,但"检查和转译分工"的思路不变。
单文件转译处理不了的写法
// 文件:types.ts
export interface User {
id: number
}
export const VERSION = 1
// 文件:index.ts
export { User } from './types' // ❌ 转译工具不知道 User 是类型,会原样保留这行导出
export { VERSION } from './types'
转译 index.ts 时看不到 types.ts,export { User } 被当作值导出保留下来;而 types.ts 编译后根本没有 User,运行时就会报"找不到导出"。改成 export type { User } from './types',转译工具看到 type 就知道可以删掉。
同类的问题还有:
- 引用
.d.ts里声明的 const enum:要知道枚举的值才能把引用内联成常量,而声明文件里的枚举运行时并不存在,见 enum 的问题。在普通 .ts 文件里定义的 const enum,转译工具会当作普通 enum 输出,不受影响 - 全局脚本里的 namespace:没有 import / export 的文件不是模块,单文件转译无法正确处理
isolatedModules 和 verbatimModuleSyntax
isolatedModules 不改变编译结果,只是让 tsc 对"单文件转译处理不了"的写法报错。上面的 export { User } 会报 TS1205,要求改成 export type。用 Vite、esbuild 等工具的项目都应该开启,Vite 文档也建议开启。
verbatimModuleSyntax(TS 5.0 起)规则更简单:没有 type 修饰的导入导出原样保留,带 type 的整条删除,编译器不再自己判断哪些导入只用作类型。它包含了 isolatedModules 的检查,而且只用作类型的导入不写 type 也会报错:
// 文件:use.ts
// ❌ 写成 import { User, VERSION } from './types' 会报 TS1484:User 是类型,必须用仅类型导入
import { type User, VERSION } from './types' // ✅ 行内 type 修饰,编译后只剩 import { VERSION }
import type { User as Admin } from './types' // ✅ 整条仅类型导入,编译后整行删除
const user: User = { id: VERSION }
const admin: Admin = user
好处是"写的是什么,输出就是什么",不同工具的输出结果一致。新项目推荐开启。
Node.js 直接运行 .ts
Node.js 内置了类型剥离(v23.6.0 和 v22.18.0 起默认开启),可以直接运行 .ts 文件。它的做法和转译工具类似但更保守:
- 只把类型标注替换成空白,不做类型检查,也不读取 tsconfig,
paths别名不生效 - 不支持需要生成代码的语法:enum、带运行时代码的 namespace、参数属性、装饰器等
- 仅类型的导入必须写
import type;相对导入必须写完整扩展名,如./util.ts;不支持 .tsx - 不处理
node_modules里的 .ts 文件
想让代码同时适用于类型剥离,可以在 tsconfig 里开启 erasableSyntaxOnly(禁止上述需要生成代码的语法)、verbatimModuleSyntax,以及允许导入路径写 .ts 的 rewriteRelativeImportExtensions。Node.js 的其他新能力见 Node.js 近几年的新能力。
代码示例:常见的工程组合
{
"scripts": {
"dev": "vite",
"typecheck": "tsc --noEmit",
"build": "tsc --noEmit && vite build"
}
}
- 前端应用:构建工具负责转译和打包,
tsc --noEmit放在 build 脚本和 CI 里;开发时编辑器实时提示,也可以另开tsc --noEmit --watch - npm 库:用打包工具或 tsc 输出 JS,用 tsc(如
--emitDeclarationOnly)输出.d.ts - Node.js 服务:开发时用类型剥离或 tsx 这类工具直接运行,CI 里跑
tsc --noEmit;生产环境可以先用 tsc 编译成 JS 再部署
面试官可能追问
只用 esbuild 不跑 tsc 会怎样?
代码照样能编译、能运行,但所有类型错误都被忽略,类型就只剩编辑器里的提示,没人看就等于没有。必须在 CI 或构建脚本里跑 tsc --noEmit,类型检查失败就阻止合并和发布。
import type 和普通 import 只导入类型,输出有什么不同?
不开 verbatimModuleSyntax 时,tsc 会自动删除只用作类型的导入,两者输出一样;但单文件转译工具不一定能判断。开启 verbatimModuleSyntax 后规则固定:普通 import 一定保留,import type 一定删除。如果一个模块有副作用(比如注册全局样式),只写了类型导入,它在运行时就不会被加载。
isolatedDeclarations 是什么?
TS 5.5 加入的选项,要求导出的函数、变量都写明显式的类型标注,这样不需要类型推断就能逐个文件生成 .d.ts。思路和 isolatedModules 一样,是为了让其他工具能并行、单文件地生成声明文件,适合大型库和 monorepo。
易错点
- Babel、esbuild、swc 不做类型检查,构建成功不代表没有类型错误
- 重导出类型要写
export type,否则单文件转译后运行时报找不到导出 - Node.js 类型剥离不读 tsconfig,
paths别名和 enum 都不能用 - isolatedModules 只是报错提示,不改变输出;verbatimModuleSyntax 会改变导入导出的保留规则
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。