Monorepo 与类型共享
Monorepo 与类型共享
写在前面
这是 TypeScript 系列的最后一篇。当公司有多个 TypeScript 项目,类型怎么共享?——是 monorepo 内部直接引用,还是发布到 npm 共享?怎么让所有项目都能用上最新的类型?
本篇会讲清楚 monorepo 的类型共享、TypeScript Project References、dts 产物生成、版本发布流程,以及公司内部 SDK 的完整类型发布实战。
一、Monorepo 中的类型共享模式
1.1 三种模式
| 模式 | 适用 | 工具 |
|---|---|---|
| workspace 直接引用 | 内部项目 | pnpm/yarn workspace |
| 构建产物引用 | monorepo 内的 SDK/UI 库 | tsc -b / 打包器 |
| 发布到 npm | 跨 monorepo 或对外 | npm publish + changesets |
1.2 项目结构
monorepo/
├── pnpm-workspace.yaml
├── tsconfig.base.json # 共享 TS 配置
├── packages/
│ ├── ui-library/ # UI 组件库(发布到 npm)
│ ├── sdk/ # 内部 SDK(发布到内部 registry)
│ └── config/ # 共享配置(eslint, tsconfig)
└── apps/
├── web-app/
└── admin-app/
二、workspace 直接引用
2.1 pnpm-workspace 配置
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
2.2 直接引用
// apps/web-app/package.json
{
"name": "web-app",
"dependencies": {
"@internal/sdk": "workspace:*",
"@internal/ui-library": "workspace:*"
}
}
// apps/web-app/src/index.ts
import { createUser } from "@internal/sdk"; // 直接用本地包
import { Button } from "@internal/ui-library";
优点:简单,改代码立刻生效,不需要发布 缺点:所有项目必须 monorepo,不能跨 monorepo 用
2.3 类型同步
直接引用 workspace 时,类型也是实时同步的——SDK 改了类型,引用方立刻看到。
三、TypeScript Project References
3.1 基础概念
Project References 把大型 TS 项目拆成多个子项目,每个子项目独立编译,产生独立的 .d.ts 和 .js。
packages/sdk/
├── tsconfig.json
├── src/
│ └── index.ts
└── dist/
├── index.js # 编译产物
├── index.d.ts # 类型声明
└── tsconfig.tsbuildinfo
3.2 子项目 tsconfig
// packages/sdk/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true, // 必须 true
"declaration": true, // 必须生成 .d.ts
"declarationMap": true, // .d.ts.map 便于跳转
"outDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"include": ["src/**/*"]
}
3.3 根 tsconfig
// tsconfig.json
{
"files": [],
"references": [
{ "path": "./packages/sdk" },
{ "path": "./packages/ui-library" },
{ "path": "./apps/web-app" }
]
}
3.4 编译
# 编译所有子项目
tsc --build
# 增量编译(只编改过的)
tsc --build --watch
# 强制全量
tsc --build --force
3.5 实战:跨包类型
// packages/sdk/src/index.ts
export interface User {
id: string;
name: string;
email: string;
}
export async function createUser(input: Omit<User, "id">): Promise<User> {
// ...
}
// apps/web-app/src/index.ts
import { createUser, type User } from "@internal/sdk";
async function main() {
// 完整类型补全
const user: User = await createUser({ name: "Alice", email: "a@x.com" });
console.log(user.id);
}
四、打包发布物
4.1 三种发布格式
| 格式 | 文件后缀 | 适用 |
|---|---|---|
| ESM | .mjs / .js(ESM) |
现代打包器(Vite / Webpack 5 / Rspack) |
| CJS | .cjs / .js(CJS) |
Node.js / 老 webpack |
| TypeScript | .ts |
内部使用 |
4.2 package.json 配置
{
"name": "@internal/sdk",
"version": "1.2.3",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": ["dist"],
"scripts": {
"build": "tsc && tsc -p tsconfig.esm.json && node scripts/build.js",
"prepublishOnly": "npm run build && npm test"
}
}
4.3 双构建配置
CJS 配置
// packages/sdk/tsconfig.json (默认)
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
ESM 配置
// packages/sdk/tsconfig.esm.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"outDir": "./dist/esm",
"tsBuildInfoFile": "./dist/.tsbuildinfo.esm"
}
}
4.4 实战:用 tsup 简化发布
tsup 是基于 esbuild 的打包工具,内置 DTS 生成:
// packages/sdk/tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true, // 自动生成 .d.ts
sourcemap: true,
clean: true,
splitting: false, // 库打包通常不分割
treeshake: true,
});
// package.json
{
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
}
}
一行命令出 ESM + CJS + 类型声明——比手写 tsc -b 简单太多。
五、内部 npm registry
5.1 为什么需要内部 registry
公司 SDK 不能发到 npmjs.com(安全 / 速度 / 私密)——需要内部 npm registry。
5.2 选型
| 工具 | 特点 |
|---|---|
| Verdaccio | 轻量级,Node.js,适合中小团队 |
| Nexus Repository | 功能多,适合大型企业 |
| Artifactory | 通用制品管理,JFrog 出品 |
| GitHub Packages | GitHub 内置,免费额度够用 |
5.3 Verdaccio 部署
# 安装
npm install -g verdaccio
# 启动
verdaccio
# 默认 4873 端口
# verdaccio/config.yaml
storage: /var/lib/verdaccio/storage
auth:
htpasswd:
file: /var/lib/verdaccio/htpasswd
packages:
"@internal/*":
access: $authenticated
publish: $authenticated
proxy: npmjs
uplinks:
npmjs:
url: https://registry.npmjs.org/
listen: 0.0.0.0:4873
5.4 发布
# 登录内部 registry
npm login --registry=http://npm.internal.company.com
# 发布
npm publish --registry=http://npm.internal.company.com
5.5 使用方配置
# 设置内部 registry 作为默认
npm config set registry http://npm.internal.company.com
# 或者项目级 .npmrc
# .npmrc
registry=http://npm.internal.company.com/
@internal:registry=http://npm.internal.company.com
@myorg:registry=http://npm.internal.company.com
只在 @internal/* 和 @myorg/* 走内部 registry——其他包走 npmjs。
六、版本管理 - Changesets
6.1 为什么需要 Changesets
monorepo 有多个包,改一个会影响多个。Changesets 帮你自动:
- 让开发者描述改动
- 自动决定哪些包需要升级,什么版本
- 自动生成 CHANGELOG
- 自动提交 commit
6.2 安装
pnpm add -D -w @changesets/cli
pnpm changeset init
6.3 添加 changeset
pnpm changeset
交互式选择:
# 哪些包变化了?
🐶 @internal/sdk (minor)
🦊 @internal/ui-library (patch)
# 描述
"新增 Button loading 状态,优化错误处理"
生成 .changeset/happy-dogs-sing.md:
---
"@internal/sdk": minor
"@internal/ui-library": patch
---
新增 Button loading 状态,优化错误处理
6.4 合并 PR
CI 检测到 changeset 后:
pnpm changeset version # 自动升级版本 + 生成 CHANGELOG
生成:
<!-- packages/sdk/CHANGELOG.md -->
## 1.2.0
### Minor Changes
- 新增 Button loading 状态,优化错误处理
6.5 发布
pnpm changeset publish # 发布所有有 changeset 的包
全自动化——开发者只关心写 changeset。
6.6 CI 集成
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://npm.internal.company.com
- run: pnpm install --frozen-lockfile
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pnpm changeset version
pnpm changeset publish
git push --follow-tags
七、类型版本管理
7.1 类型版本独立
库的代码变了,但类型不变,要不要发 minor?
原则:类型变更 = minor / major(因为影响用户代码),只是改 bug = patch(如果类型不变)。
7.2 标记类型变更
// @internal/sdk CHANGELOG.md
## 1.2.0
### Minor Changes
- `getUser` 返回类型从 `User | null` 改为 `User | undefined`(修复 null 误用)
- 新增 `User.id` 字段类型为 `string`(原来是 `string | number`)
7.3 类型测试
类型也应该测试——验证类型推断正确。
// packages/sdk/__tests__/types.test-d.ts
import { expectType, expectError } from "tsd";
import { createUser, type User } from "../src/index";
expectType<Promise<User>>(
createUser({ name: "Alice", email: "a@x.com" })
);
expectError(
createUser({ name: "Alice" }) // 缺 email
);
// package.json
{
"scripts": {
"test": "tsd"
}
}
tsd 库专门做类型测试。
八、实战:内部 SDK 完整发布流程
8.1 SDK 项目结构
packages/sdk/
├── src/
│ ├── index.ts
│ ├── users.ts
│ └── orders.ts
├── __tests__/
│ ├── users.test.ts
│ └── types.test-d.ts
├── CHANGELOG.md
├── package.json
├── tsup.config.ts
└── tsconfig.json
8.2 package.json
{
"name": "@internal/sdk",
"version": "1.0.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:types": "tsd",
"lint": "eslint src --max-warnings=0",
"prepublishOnly": "npm run lint && npm run test && npm run build && npm run test:types"
},
"devDependencies": {
"tsup": "^8.0.0",
"tsd": "^0.31.0",
"vitest": "^1.6.0",
"typescript": "^5.3.0"
},
"publishConfig": {
"registry": "http://npm.internal.company.com"
}
}
8.3 完整的发布流程
# 1. 开发
git checkout -b feature/new-api
# 2. 写代码 + 测试
vim src/users.ts
vim __tests__/users.test.ts
# 3. 跑测试
pnpm test
pnpm test:types
pnpm lint
# 4. 添加 changeset
pnpm changeset
# 选择 @internal/sdk,版本 minor
# 写描述:"新增 listUsers API"
# 5. 提交 + PR
git add .
git commit -m "feat: 新增 listUsers API"
gh pr create
# 6. Code Review + 合并
# (Reviewer 确认 changeset 描述)
# 7. CI 自动发布
# 合并到 main 后,GitHub Actions:
# - pnpm changeset version (升级版本 + 生成 CHANGELOG)
# - pnpm changeset publish (发布到内部 registry)
# - git push (提交版本变更)
8.4 使用方自动升级
# 50 个引用方项目升级 SDK 版本
pnpm update @internal/sdk --latest
# 或者用 Renovate / Dependabot 自动 PR
整个发布 + 升级流程全自动——开发者只写 changeset。
九、内部类型共享的几个最佳实践
9.1 共享配置
// packages/config/tsconfig.base.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"noEmit": true
}
}
// apps/web-app/tsconfig.json
{
"extends": "@internal/config/tsconfig.base.json",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"]
}
9.2 共享工具类型
// packages/utils-types/src/index.ts
export type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
export type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
export type Nullable<T> = T | null;
export type Optional<T> = T | undefined;
// apps/web-app/package.json
{
"dependencies": {
"@internal/utils-types": "workspace:*"
}
}
9.3 API 契约类型
// packages/contracts/src/index.ts
export interface User {
id: string;
name: string;
email: string;
}
export interface ApiResponse<T> {
code: 0;
data: T;
} | {
code: 1;
message: string;
fields?: string[];
}
前后端共用——后端实现按这个类型,前端按这个类型调用。
十、TS 5.x 的 monorepo 改进
10.1 isolatedDeclarations(TS 5.5+)
{
"compilerOptions": {
"isolatedDeclarations": true
}
}
要求每个文件的类型可以独立推断——为未来 Go 重写编译器(不需要看 import)做准备。
// NG - isolatedDeclarations 下报错
export function process(x) { // 隐式 any
return x.toString();
}
// OK
export function process(x: number): string {
return x.toString();
}
10.2 没有 isolatedDeclarations 的项目
- 项目库大小 < 50 文件:可以
- 项目库大小 > 100 文件:建议开
- monorepo 项目:强烈建议开(编译快很多)
十一、本篇小结
| 主题 | 关键点 |
|---|---|
| workspace 引用 | monorepo 内部最简单 |
| Project References | 大型 monorepo 必备 |
| 打包发布 | tsup / tsc -b + 双构建 |
| 内部 registry | Verdaccio / GitHub Packages |
| Changesets | 版本管理 + CHANGELOG |
| 类型测试 | tsd 验证类型 |
| 工具 | 适用 |
|---|---|
| pnpm workspace | monorepo 基础 |
| tsup | 库打包(快速) |
| tsc -b | 库打包(原生) |
| Verdaccio | 内部 registry |
| Changesets | 版本发布 |
| tsd | 类型测试 |
到这里 TypeScript 进阶系列 9 篇全部完成。
系列回顾
- ✅ 类型系统基础与心智模型
- ✅ 联合类型与类型守卫
- ✅ 泛型与条件类型
- ✅ 模板字面量类型
- ✅ 声明文件与 .d.ts
- ✅ tsconfig 调优与工程配置
- ✅ 类型编程在业务中的实战
- ✅ TS 5.x 新特性深度解读
- ✅ Monorepo 与类型共享
到这里,四个系列全部完成。最后让我创建文章生成 Skill,让你后续可以按规范续写。