vite build 不查类型! 检查归 tsc --noEmit, 转译归 esbuild/swc — 两道工序分开管
TypeScript 生态把"编译"拆成了两件事: 类型检查与代码转译。tsc 是全程序检查器(慢而完整), esbuild/swc 是逐文件转译器(快如闪电但不查类型)。Vite 默认用后者出产物 — 所以 vite build 通过只说明"能转译", 不说明"类型没问题"。正确的姿势是 CI 里两条腿: tsc --noEmit 管类型门禁, vite build 管产物。
tsconfig 里的每个开关都对应这条链路的某个环节: strict 家族管检查严度; target/lib 管语法降级与可用 API; module/moduleResolution 管模块怎么被解析(必须与打包器一致); isolatedModules 是"我的代码保证能被逐文件转译"的承诺。配置不是玄学, 是链路契约。
// package.json { "scripts": { "typecheck": "tsc --noEmit", "build": "tsc --noEmit && vite build" } }
{ "strict": true }
// 等价于同时打开五个最严格的开关{ "target": "ES2022", "lib": ["ES2022", "DOM"] }
// 用新 API 而 lib 没声明 → "xxx 不存在"假报错{ "module": "ESNext",
"moduleResolution": "bundler" } // vite 应用// ✗ isolatedModules 下: export { SomeType } from './x'; // ✓ 必须显式: export type { SomeType } from './x';
// vite.config.ts resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }
{ "skipLibCheck": true }
// 副作用: 依赖类型包的错误被掩盖{ "include": ["src"] }
// src 外的 legacy 被 src import → 照样全量检查{ "references": [{ "path": "./packages/core" }] }
// tsc -b --build --verbose 增量只编改动包{ "declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist/types" }{ "verbatimModuleSyntax": true }
// import type { User } from ... 必须写 type{ "composite": true, "incremental": true }
# CI: actions/cache 存 *.tsbuildinfovite build 全绿, 上线后 optional chaining 用在 number 上崩了。根因: 从没人跑过 tsc:
# .github/workflows/ci.yml - run: pnpm typecheck # tsc --noEmit - run: pnpm test - run: pnpm build # vite build # 本地 husky pre-push 也跑 typecheck, 双保险
上线首周抓出 17 个被转译器吞掉的类型错误。
12 万行旧代码开 strict 等于停摆三个月。新代码 strict, 旧代码维持, 边界渐进推进:
// tsconfig.json (基础, 不开 strict) { "extends": "./tsconfig.base.json", "include": ["src-old"] } // tsconfig.strict.json { "extends": "./tsconfig.base.json", "compilerOptions": { "strict": true }, "include": ["src-new"] } // CI: 新目录强制, 旧目录每季度迁移 10%
20 个包每次全量检查 6 分钟。references + composite 后只重查被改的包链:
// 根 tsconfig.json { "files": [], "references": [ { "path": "packages/core" }, { "path": "packages/ui" }, { "path": "apps/web" } ] } # tsc -b ← 按依赖拓扑增量构建
全量 6min → 增量 40s。
tsc 检查通过, vite dev 却报 cannot resolve '@/lib/x'。tsconfig paths 只对 tsc 生效:
// vite.config.ts 必须同步: import { fileURLToPath } from 'node:url'; export default { resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, }; // jest 侧还要 moduleNameMapper — 每个工具各配一次
手写声明与实现漂移, 用户编译报错但运行正常, 售后噩梦。改为声明自动生成:
{ "compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist" },
"include": ["src"] }
// package.json: "types": "./dist/index.d.ts"
// CI: 消费侧 tsc 编译通过作为发布门槛
全量检查 90s, 其中 70s 在检查 node_modules 的 .d.ts。开启后 20s, 但要单独守护自家声明:
{ "skipLibCheck": true }
// 库项目额外跑一次不带 skip 的检查自家 d.ts:
// tsc --noEmit --skipLibCheck false dist/index.d.ts
// 锁定关键依赖的类型版本(package-lock)防漂移
开启后 barrel 里裸类型 re-export 全部报错。批量改成显式 type 导出:
// 错: export { User, Order } from './types'; // 对: export type { User, Order } from './types'; // const enum 同理被禁 → 换 as const 对象: const Kind = { a: 'a' } as const; type Kind = typeof Kind[keyof typeof Kind];
target ES5 导致 ?? 可选链全转译, 产物肥 30%; 但只调 target 不够, polyfill 是另一层:
{ "target": "ES2020" } // 保留 ?? 与 ?., 现代浏览器直跑
// 老浏览器兼容交给 @vitejs/plugin-legacy / core-js:
// polyfill 由打包器注入, ts target 只管语法降级
产物 -28%, Lighthouse 脚本解析时间同步下降。
库要双格式输出, 用两个 tsconfig 各自 emit, exports 字段分流:
// tsconfig.esm.json: module ESNext, outDir dist/esm // tsconfig.cjs.json: module CommonJS, outDir dist/cjs # tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json // package.json exports: { import: dist/esm, require: dist/cjs }
大仓 tsc 峰值 6GB, IDE 卡死。方案: 分项目 references + CI 缓存 + 必要时 fork:
# webpack 项目: thread 化类型检查 ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); // vite 项目: vite-plugin-checker 同理, 不阻塞转译主进程 // NODE_OPTIONS=--max-old-space-size=8192 tsc --noEmit
tsc --noEmit 门禁。 "build": "tsc --noEmit && vite build"
// vite: resolve.alias · jest: moduleNameMapper · tsx: tsconfig// legacy 排除了, 但 src import 了它 → 全量检查又回来了{ "strict": true } // 别只开一半tsc --noEmit dist/index.d.ts # 发布门槛{ "include": ["src", "test"] } # 别写 "**/*"export type {} 显式化。 // 错: export { User } from './u'; // 对: export type { User } from './u';
const S = { on: 'on' } as const; type S = typeof S[keyof typeof S];
{ "target": "ES2020" } # 产物 -30% 常见{ "lib": ["ES2022", "DOM", "DOM.Iterable"] }{ "moduleResolution": "bundler" } # vite 应用import type。 import type { User } from './user';
{ "allowJs": true, "checkJs": false } # 先宽松cache:
paths: ['**/*.tsbuildinfo']{ "composite": true } # 子包 tsconfig 必配{ "emitDeclarationOnly": true }npx tsx src/main.ts # 与 build 同源转译器{ "noUnusedLocals": true } # 与 strict 分开评估{ "extends": "./tsconfig.base.json", "include": ["test"] }{ "paths": { "@/*": ["./src/*"] } } # 不配 baseUrl 也能用