TypeScript支持从零搭建到项目落地的完整实践指南
优化前:卡得不行
上周上线一个带类型提示的组件库文档站,本地 dev server 启动要 5.2 秒,热更新(HMR)平均延迟 3.8 秒,改一行 interface 就得等 4 秒才看到效果。最离谱的是 VS Code 的 TypeScript Server 每次自动重载后,整个编辑器光标卡顿、智能提示失灵,我甚至怀疑自己是不是开了什么全家桶插件——关了所有插件,问题照旧。
不是项目小,是它真小:就 12 个组件,TypeScript 声明文件加起来不到 800 行。但 node_modules 里 @types/react + typescript + ts-jest + vue-tsc(我们混用了 Vue 和 React 组件)硬生生把 TS 服务拖进泥潭。跑 tsc --noEmit --watch 直接 CPU 占满,风扇起飞。
找到痛点了!
先用 tsc --traceResolution 看了一眼模块解析路径,发现每次改动都触发全量重新检查——哪怕只改了一个 .d.ts 文件,TS 都会从 node_modules/@types/ 往上翻所有依赖树。又试了 typescript --explainFiles,输出里赫然列出 1700+ 个被包含的文件,其中 1264 个来自 @types/react 和它的“亲戚” @types/react-dom、@types/react-router……但我们的组件库根本不用 react-router,只用 React.FC 和几个 hooks。
再开 VS Code 的 TS Server 日志(Developer: Toggle Developer Tools → Console),搜 Project loading,发现每次保存都触发 updateGraphWorker,耗时稳定在 3200ms±200ms。定位很清晰:不是代码写得烂,是 TS 在做太多它本不必做的事。
试了几种方案
- 删
@types/react-router?删了,没用。因为@types/react-dom依赖@types/react,而后者又通过typesVersions主动引入一堆 polyfill 类型,删不干净。 - 用
skipLibCheck: true?跳过声明文件检查,确实快了——启动降到 1.4 秒。但代价是类型安全裸奔,React.ReactNode写成React.NodeReact都不报错,上线前 CI 里tsc --noEmit一跑直接挂,放弃。 - 拆成多个
tsconfig.json?搞了个tsconfig.build.json专用于构建,和tsconfig.dev.json分离。有用,但没根治。VS Code 默认读根目录tsconfig.json,除非你手动告诉它用哪个——太反人类,团队新人一来就懵。
折腾了半天发现:真正的病灶不在依赖多,而在 TS 默认把所有 node_modules/@types/* 全部纳入程序图(program graph)。而我们只需要确保组件导出的类型能被正确消费,不需要让 TS 去“理解”整个 React 生态的类型定义。
最后这个效果最好:用 types 字段精准收口
核心思路:不让 TS 自动扫描 node_modules/@types,而是显式声明“我只信任这几个类型包”。改 tsconfig.json:
{
"compilerOptions": {
"types": ["react", "react-dom"],
"typeRoots": ["./node_modules/@types"]
}
}
注意:删掉 lib 里冗余项,只留刚需。我们组件库是纯类型声明 + Markdown 文档渲染,根本不用 DOM API 或 ES2022 新语法,所以:
{
"compilerOptions": {
"lib": ["ES2020", "DOM"],
"types": ["react", "react-dom"]
}
}
别写 ["ES2023", "DOM", "WebWorker", "ScriptHost"] 这种全家桶,每多一个 lib,TS 就得多加载几百个内置声明文件。
更关键的是——禁用自动类型获取。在 tsconfig.json 里加这行:
{
"compilerOptions": {
"types": ["react", "react-dom"],
"typeRoots": ["./node_modules/@types"],
"allowSyntheticDefaultImports": false,
"skipDefaultLibCheck": true
}
}
这里 skipDefaultLibCheck 是重点:它跳过对 lib.dom.d.ts 等默认库的完整性校验,但不跳过类型检查本身。实测下来,既保住了 HTMLElement 这类基础类型可用,又砍掉了 90% 的无意义校验开销。
还有个小技巧:把组件库自己的类型声明(比如 index.d.ts)放到单独目录,然后在 tsconfig.json 里用 include 精确控制:
{
"include": ["src/components/**/*", "types/index.d.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
别用 "include": ["**/*"],那是自找死路。
优化后:流畅多了
改完立刻测:
- VS Code TS Server 重载时间:从 3200ms → 420ms(降幅 87%)
- dev server 启动:5.2s → 840ms
- HMR 响应:3.8s → 790ms(改一个
interface,0.8 秒内提示更新完成) - CPU 占用峰值:100% → 22%
顺手跑了一遍 tsc --noEmit --watch,内存占用从 1.4GB 降到 360MB。现在边敲代码边喝咖啡,风扇安静得我以为它坏了。
当然,不是完美。比如某个同事非要在组件里写 import { createBrowserRouter } from 'react-router-dom',那 TS 还是会去拉它的类型——但这是业务代码问题,不是配置问题。我们文档站本来就不该 import 路由,CI 里加个 ESLint 规则 no-restricted-imports 拦住就行。
性能数据对比
下面是三次典型操作的耗时记录(单位:ms,取 5 次平均值):
| 操作 | 优化前 | 优化后 | 降幅 |
|---|---|---|---|
| TS Server 初始化 | 3210 | 425 | 86.8% |
| dev server 启动 | 5230 | 845 | 83.8% |
| HMR(改 .ts 文件) | 3790 | 785 | 79.3% |
tsc --noEmit 单次检查 |
2940 | 610 | 79.2% |
所有数据都是本地 M1 Mac Mini(16GB)实测。没有魔法,就是让 TS 少干点事。
踩坑提醒:这三点一定注意
- 不要乱设
typeRoots。我第一次写成"typeRoots": ["./types", "./node_modules/@types"],结果 TS 优先查 ./types,导致自己写的index.d.ts里漏了个export,编译不报错但下游消费时报Cannot find module——查了两小时才发现是路径顺序惹的祸。建议只写["./node_modules/@types"],其他类型通过types字段引入。 skipDefaultLibCheck不等于skipLibCheck。前者只跳过默认库(如lib.dom.d.ts)的校验,后者跳过全部第三方类型。我们选前者,不然React.FC的类型推导就崩了。- Vue + React 混用项目务必确认
@vue/runtime-core和@types/react的版本兼容性。我们踩过一次:升级typescript@5.3后,@types/react@18.2里的JSX.IntrinsicAttributes和 Vue 的类型冲突,报一堆Type instantiation is excessively deep。降级到@types/react@18.0.37解决。这类问题没法自动化,只能靠yarn why @types/react锁版本。
以上是我的优化经验,有更好的方案欢迎交流
这个方案不是银弹——如果你的组件库要支持 SSR、要生成 d.ts、要对接 Storybook 的类型推导,可能还得加 declaration: true 和 emitDeclarationOnly,那构建时间又会涨回来一点。但对绝大多数内部文档站、设计系统站点来说,够用了。
我也试过 tsc --incremental + tsbuildinfo,但增量构建在频繁修改类型定义时反而容易失效,不如直接砍掉冗余类型来得干脆。
如果你也在用 typescript@5.x + 多框架混写 + VS Code,欢迎评论区甩出你的 tsconfig.json 片段,一起看看还能怎么压榨。或者,你有更狠的骚操作?求分享。

暂无评论