前端开发··2 阅读·预计 7 分钟

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 enumisolatedModules 冲突——当你的代码里某个文件单独被 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 文件、用 tsconfigstrict + noUncheckedIndexedAccess 提高检查密度,再配合编辑器里的语言服务做即时反馈,形成「编辑器 → typecheck → CI」三层漏斗。

一个自检清单

  • 纯类型导入是否都用了 import type
  • 是否开启了 verbatimModuleSyntaxisolatedModules
  • 有没有误用 const enumnamespace 导致运行时残留?
  • build 脚本是否包含 tsc --noEmit
  • CI 是否独立执行了类型检查?

记住这条心智模型:esbuild 保证「能跑」,tsc 保证「没错」。只有把两者都纳入流水线,你的 TypeScript 才是真正意义上的类型安全,而不是 dev 里自我安慰的装饰。

0 评论

评论区

登录 后参与评论