Monorepo 中的 tsconfig 实践

面向 TypeScript monorepo 的 tsconfig 落地指南,重点说明开发态与构建态分层、project references 与声明生成策略。

#tech / dev #resource / typescript #type / howto #status / growing

[!info] related notes

Monorepo 中的 tsconfig 实践

目标

这篇笔记聚焦的是 TypeScript monorepo 里的一个落地问题:

怎样让开发期联调顺手,同时又不让构建期包边界失真?

默认目标不是“全仓只有一份 tsconfig”,而是:

  • 开发态解析清楚
  • 构建态边界清楚
  • 类型产物链路清楚

前置条件

在讨论具体配置前,先固定三条前提:

  1. 业务代码只写稳定包名,不跨包导入别人的 src
  2. paths 优先服务开发态和包内 alias,不要直接充当发布真值
  3. 需要对外消费的包,package.json 里的 types / exports 最终都应回到 dist

步骤

1. 根配置只放公共基线

tsconfig 更适合承担这些职责:

  • 通用编译选项
  • 严格性基线
  • 共享的 JSX / module / target 基线

它不适合成为“所有库构建都依赖的源码映射真值”。

2. 包级 tsconfig 明确自己的边界

每个 package / app 的 tsconfig 最好显式写清:

  • rootDir
  • outDir
  • composite
  • declaration
  • references

而不是完全依赖根配置隐式推断。

3. paths 优先留给包内 alias 或开发态映射

最稳的用法通常是:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

如果需要开发期跨包看源码,也更推荐把这件事放在开发态专用配置里,而不是默认让所有构建都继承它。

4. 复杂库使用双 tsconfig

对依赖面较深、入口较多、类型图较复杂的库,通常推荐:

  • tsconfig.json 服务 IDE、dev、test
  • tsconfig.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 负责 JS
  • tsc -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.jsontypesexports 指向的是构建产物
  • clean CI 不依赖本地遗留的 dist

常见问题

为什么不推荐把跨包源码 alias 当成唯一真相

因为它会同时带来三个问题:

  • IDE / tsc program 变大
  • 包边界模糊
  • 本地和真实消费者环境偏差变大

project references 和 paths 是不是只能二选一

不是。

更常见的现实做法是:

  • references 负责项目级依赖
  • paths 负责包内 alias,或少量开发态映射

什么时候一定要上双 tsconfig

通常出现在这些信号一起出现时:

  • DTS 生成慢、炸内存或类型图过深
  • 库有多入口、子路径导出、复杂跨包依赖
  • 开发态想吃源码,但构建态必须回到真实边界
创建于 2025/1/1 更新于 2026/5/27