库构建中 JS 打包与 DTS 生成分层
说明为什么复杂库常把 JavaScript 打包和 TypeScript 声明生成拆成两条链路,以及这种分层何时比 bundler 一把梭更稳。
#tech / dev
#resource / typescript
#type / synthesis
#status / growing
[!info] related notes
库构建中 JS 打包与 DTS 生成分层
范围
这篇笔记讨论的是库构建中的一个具体分层:
- JavaScript 产物由 bundler 负责
- 类型声明产物由
tsc单独负责
它关心的不是“该用哪个打包工具”,而是:
什么时候应该把 JS build 和 DTS generation 拆成两条链路?
为什么要放在一起理解
很多仓库一开始会觉得:
tsup --dtsrollup + dts 插件vite build顺手带类型
看起来最省事。
但当库开始变复杂以后,JS 打包和 DTS 生成其实会暴露出不同约束:
- JS 打包更关心运行时代码、格式、external、tree-shaking
- DTS 生成更关心类型图、跨包边界、声明入口、
paths解析
把两者强绑在一条隐式链路上,问题通常更难定位。
依赖路径 / 调用链 / 演进链
简单阶段:bundler 一把梭
在叶子包或轻量工具包里,直接:
tsup dts: true
往往足够。
因为这时:
- 入口少
- 类型图浅
- 跨包边界简单
复杂阶段:开始出现职责分裂
当包变成下面这种形态时,分层价值会迅速变大:
- 多入口
- 子路径导出
- 复杂类型导出
- 深层跨包依赖
- monorepo 内需要区分开发态和构建态解析
这时更稳的结构通常是:
- bundler 只读源码,产出
.js tsc --emitDeclarationOnly单独产出.d.ts
典型落地
一个常见组合是:
tsup/vite build/rollup负责 JStsc -p tsconfig.build.json --emitDeclarationOnly负责 DTS
构建态如果还需要更严格的边界,可以再配合:
- 构建专用
tsconfig.build.json - 切断或收窄源码级
paths - 必要时改为消费上游
dist/*.d.ts
对比与易混淆点
这不是“tsc 替代 bundler”
这里的分层不是让 tsc 接管整个 build,而是:
- bundler 继续解决运行时代码打包问题
tsc只负责类型产物
emitDeclarationOnly 的价值不只是“少生成 JS”
更重要的是:
- 让声明生成拥有自己独立的解析边界
- 让构建问题能分清是 JS 侧还是类型侧
不是所有包都值得上这套
如果包很轻,强行拆出第二条声明链路,收益可能不够。
更合理的经验法则通常是:
- 轻量包先保持简单
- 复杂库再显式分层
这套做法为什么常被认为更工程化
因为它把两个本来就不同的问题拆开了:
- JS 怎么产出
- 类型怎么产出
一旦出了问题,你更容易知道该看:
- bundler 配置
tsconfig.build.json- 声明入口与
package.json types - project references / 上游
dist
而不是面对一个“大概都在 build 里”的黑箱。