Monorepo 中的 tsconfig 实践
面向 TypeScript monorepo 的 tsconfig 落地指南,重点说明开发态与构建态分层、project references 与声明生成策略。
#tech / dev
#resource / typescript
#type / howto
#status / growing
[!info] related notes
Monorepo 中的 tsconfig 实践
目标
这篇笔记聚焦的是 TypeScript monorepo 里的一个落地问题:
怎样让开发期联调顺手,同时又不让构建期包边界失真?
默认目标不是“全仓只有一份 tsconfig”,而是:
- 开发态解析清楚
- 构建态边界清楚
- 类型产物链路清楚
前置条件
在讨论具体配置前,先固定三条前提:
- 业务代码只写稳定包名,不跨包导入别人的
src paths优先服务开发态和包内 alias,不要直接充当发布真值- 需要对外消费的包,
package.json里的types/exports最终都应回到dist
步骤
1. 根配置只放公共基线
根 tsconfig 更适合承担这些职责:
- 通用编译选项
- 严格性基线
- 共享的 JSX / module / target 基线
它不适合成为“所有库构建都依赖的源码映射真值”。
2. 包级 tsconfig 明确自己的边界
每个 package / app 的 tsconfig 最好显式写清:
rootDiroutDircompositedeclarationreferences
而不是完全依赖根配置隐式推断。
3. paths 优先留给包内 alias 或开发态映射
最稳的用法通常是:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
如果需要开发期跨包看源码,也更推荐把这件事放在开发态专用配置里,而不是默认让所有构建都继承它。
4. 复杂库使用双 tsconfig
对依赖面较深、入口较多、类型图较复杂的库,通常推荐:
tsconfig.json服务 IDE、dev、testtsconfig.build.json服务构建与声明生成
常见分工是:
- 开发态:允许解析到 workspace 源码
- 构建态:切断源码级
paths,或把它们改到上游dist/*.d.ts
5. 轻量包再考虑 bundler 一把梭
如果一个包:
- 依赖很浅
- 入口很少
- 声明图很简单
那么继续使用 tsup dts: true 之类的轻量方案也可以。
但即使如此,也最好确认构建态不会继续偷偷追到别的包源码里。
6. 用 references 管理项目级依赖
对中大型 monorepo,更推荐让 TypeScript 通过 references 理解依赖图,而不是只靠 paths 假装一切都在一个 program 里。
这通常意味着:
- 被引用包启用
composite - 需要声明输出时启用
declaration - 通过
tsc -b或相关任务串起依赖顺序
7. 把 JS 构建和 DTS 生成分开
对复杂库,更稳的构建方式往往是:
tsup/vite build/rollup负责 JStsc -p tsconfig.build.json --emitDeclarationOnly负责.d.ts
这样做的好处是:
- JS bundling 和类型产物职责清楚
- 声明生成的解析边界可控
- 出问题时更容易定位
8. 至少保留一条“消费 dist / exports”的验证链路
如果整个仓库只有源码 alias 这一个世界,本地经常会“看起来都能过”,但 CI 或真实消费者才爆炸。
因此至少要有一条链路验证:
- 不依赖 workspace 源码 alias
- 按包的
types/exports消费产物 - 能在干净环境里重现真实发布边界
验证
一个更健康的 monorepo tsconfig 结构,通常会表现出这些信号:
- 改上游库时,开发态下游能快速看到变化
- 构建态不会因为跨包源码混入而反复撞
rootDir .d.ts生成不会无限追整个 workspace 源码图package.json的types和exports指向的是构建产物- clean CI 不依赖本地遗留的
dist
常见问题
为什么不推荐把跨包源码 alias 当成唯一真相
因为它会同时带来三个问题:
- IDE /
tscprogram 变大 - 包边界模糊
- 本地和真实消费者环境偏差变大
project references 和 paths 是不是只能二选一
不是。
更常见的现实做法是:
references负责项目级依赖paths负责包内 alias,或少量开发态映射
什么时候一定要上双 tsconfig
通常出现在这些信号一起出现时:
- DTS 生成慢、炸内存或类型图过深
- 库有多入口、子路径导出、复杂跨包依赖
- 开发态想吃源码,但构建态必须回到真实边界