Vite 的转译与类型检查分离:为什么 dev 能跑通、CI 却会炸
很多开发者在 Vite 项目里都遇到过这种灵异现象:npm run dev 一切正常,页面跑得飞快,但推到 CI 上 tsc --noEmit 直接红了。根本原因只有一个——Vite 的转译(transpile)和类型检查(type-check)是两条完全独立的链路。理解这条边界,决定了你的 TypeScript 到底是「编译期保安」还是「运行时地雷」。
esbuild 只擦除,不检查
Vite 在开发态和构建态都用 esbuild 处理 TypeScript,而 esbuild 的策略极其朴素:抹掉类型注解,其余原样保留。它不是编译器,而是「类型擦除器」。
// 源码
const greet = (name: string): number => name.length
// esbuild 转译后的产物
const greet = (name) => name.length
esbuild 从不去验证 name.length 返回的是不是 number。哪怕你写错了类型,只要语法合法,它照样转译通过。这就是为什么 dev 环境永远「能跑」——因为你写的类型错误根本没有被任何人检查。
正例:类型导入必须显式标记
最容易踩坑的是「纯类型导入」。TypeScript 和 esbuild 对导入的处理方式不同:
// 反例:普通导入,esbuild 无法判断是否为纯类型
import { User } from './types' // User 只是 interface
const u: User = { id: 1 }
如果 User 只是一个 interface,运行时 ./types 其实没有任何导出,import 语句一旦被保留,就会在浏览器里报 SyntaxError: The requested module does not provide an export named 'User'。
// 正例:显式 inline type 标记
import type { User } from './types'
const u: User = { id: 1 }
import type 让 esbuild 明确知道这行可以整个删掉。这正是 verbatimModuleSyntax 这个编译选项存在的原因——它强制所有纯类型导入/导出都必须显式 type 标记,把「擦除边界」从约定变成强制。
反例:const enum 与 namespace 的隐藏体
更隐蔽的是 const enum。它会在编译期被内联,但 esbuild 默认不内联 const enum,而是把它当作普通 enum 转译,产出一段运行时的对象代码。
// const enum 声明
export const enum Status {
Ready = 0,
Done = 1,
}
tsc 会把 Status.Ready 直接内联成字面量 0,产物零残留。而 esbuild 会保留一个带反向映射的对象:
// esbuild 的转译产物(简化)
var Status = ((Status2) => {
Status2[Status2["Ready"] = 0] = "Ready"
Status2[Status2["Done"] = 1] = "Done"
return Status2
})(Status || {})
这不仅是体积问题,更会导致 const enum 与 isolatedModules 冲突——当你的代码里某个文件单独被 esbuild 处理、而另一处被 tsc 内联时,同名的 Status 可能一个存在、一个不存在,产生「运行时未定义」的诡异 bug。这也是官方不建议在 isolatedModules 项目里使用 const enum 的原因。
建立工程化防线
既然转译和检查天然分离,工程上的解法就是把两条链路都显式地跑满:
// package.json
{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build", // 关键:先检查再构建
"typecheck": "tsc --noEmit"
}
}
tsc --noEmit 是这条防线的核心——它只做类型检查,不产出文件,恰好补齐了 esbuild 缺失的那一半。把它放进 build 命令(甚至用 CI 门禁强制),类型错误就无法再藏到 dev 的「假绿灯」后面。
更进一步,用 vue-tsc 处理 .vue 文件、用 tsconfig 的 strict + noUncheckedIndexedAccess 提高检查密度,再配合编辑器里的语言服务做即时反馈,形成「编辑器 → typecheck → CI」三层漏斗。
一个自检清单
- 纯类型导入是否都用了
import type? - 是否开启了
verbatimModuleSyntax或isolatedModules? - 有没有误用
const enum或namespace导致运行时残留? -
build脚本是否包含tsc --noEmit? - CI 是否独立执行了类型检查?
记住这条心智模型:esbuild 保证「能跑」,tsc 保证「没错」。只有把两者都纳入流水线,你的 TypeScript 才是真正意义上的类型安全,而不是 dev 里自我安慰的装饰。
评论区
登录 后参与评论