5月31日 16:17

Vite 为什么不默认做 TypeScript 类型检查?

Vite 支持 TypeScript,但默认只转译,不做完整类型检查。很多人迁移后会疑惑:页面能跑,tsc --noEmit 却报错。原因是 Vite 把开发反馈速度放在前面,类型检查需要理解整个项目的类型图,如果塞进每次模块转换,HMR 会明显变慢。

Vite 负责转译,不负责兜底

.ts.tsx 文件会被 esbuild 快速转成 JavaScript,类型标注会被擦除,JSX 和新语法也会被处理。但 esbuild 不会像 TypeScript 编译器那样做完整类型分析,所以“Vite 能运行”不等于“类型安全”。

ts
type User = { id: number } const user: User = { id: '1' } // 需要 tsc 才能稳定拦住

tsconfig 怎么配

Vite 项目建议使用 moduleResolution: "bundler",它更贴近现代打包器对 ESM、exports 和条件导出的解析方式。isolatedModules 建议打开,能提前暴露不适合单文件转译的写法。noEmit 让 TypeScript 只检查,不输出文件,避免和 Vite 构建产物混在一起。

json
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "isolatedModules": true, "noEmit": true, "skipLibCheck": true }, "include": ["src", "vite.config.ts"] }

skipLibCheck 是现实取舍。业务项目开启它可以减少依赖声明文件带来的噪音和耗时;基础库、SDK 或类型要求极高的项目要谨慎,因为它可能掩盖第三方类型问题。

类型检查放到哪里

本地开发可以依赖编辑器提示,也可以接入 vite-plugin-checker。团队协作时,CI 必须执行 tsc --noEmit,否则有人关闭编辑器提示后,类型错误仍可能进入主干。

ts
import checker from 'vite-plugin-checker' export default defineConfig({ plugins: [checker({ typescript: true })] })

路径别名要同时配置 Vite 和 TypeScript。resolve.alias 影响运行和构建,paths 影响编辑器和类型检查;只配一边,就会出现能运行但编辑器报错,或编辑器正常但构建失败。

追问

为什么 Vite 不直接检查类型?

因为 Vite 的核心链路是按需转译模块,而完整类型检查是项目级静态分析。把它放进每次请求,会拖慢冷启动和 HMR。取舍是先获得运行反馈,再用编辑器、checker 或 CI 保证类型安全。踩坑点是把 Vite build 通过当成类型通过。

vite-plugin-checkertsc --noEmit 怎么选?

本地想尽快看到错误,可以用 vite-plugin-checker。上线前要稳定兜底,应该跑 tsc --noEmit。插件更贴近开发体验,但大项目里也会占内存和 CPU。边界是合并门禁不要依赖某个开发者的编辑器。

isolatedModules 为什么会让一些写法报错?

它要求每个文件都能被独立安全转译,这和 esbuild 的工作方式一致。只导出类型却按值导入、部分 namespace 写法,都可能因此暴露问题。限制看起来烦,但能提前发现运行时会消失的类型代码。取舍是少一点 TS 写法自由,换来构建链路一致。

路径别名为什么要配两份?

Vite 和 TypeScript 是两个解析系统。只配 Vite,编辑器和 tsc 可能找不到模块;只配 TS,构建时可能解析失败。很多人以为是缓存问题,其实是配置源不一致。monorepo 里还要看 workspace 包的 exports 和声明文件。

生产构建前还需要 ESLint 吗?

需要,ESLint 和 TypeScript 关注点不同。TS 主要检查类型关系,ESLint 更擅长发现 Hook 依赖、未处理 Promise、禁用 API 和潜在逻辑问题。取舍是规则越多 CI 越慢,所以要区分阻断规则和建议规则。不要把 lint 只当格式化工具。

标签:Vite