共享包与内部依赖:类型/工具一处定义全仓复用
本节目标
- 把公共 TS 类型抽成独立包,全仓 import
- 用
import type只引类型不引运行时代码 - 用 base tsconfig 统一全仓 TS 配置
Monorepo 最实在的收益,是「公共东西只定义一次」。典型共享包有三种:
- 类型包(如
@my/types):全仓共用的领域模型、API 响应类型。 - 工具包(如
@my/utils):格式化、校验等纯函数。 - 配置包(如
@my/eslint-config、@my/tsconfig):统一的代码规范。
以类型包为例,做法如下:
// packages/types/src/index.ts
// 运行环境:Node.js 18+ + typescript;类型检查 pnpm --filter @my/types exec tsc --noEmit
export interface User {
id: number
name: string
role: 'admin' | 'user'
}
export interface ApiResponse<T> {
code: number
data: T
message: string
}web 和 admin 都 pnpm add @my/types@workspace:*,然后:
// packages/web/src/api.ts
import type { User, ApiResponse } from '@my/types' // 用 import type 只引类型
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`)
return res.json()
}注意这里用的是 import type:它明确告诉 TS「只导入类型、不导入值」。打包时这些导入会被完全抹掉,不会把类型包打进运行时代码,也不会造成循环依赖。User 定义改一处,所有引用它的包的类型检查立刻同步——这就是「类型单一来源」。
配置也要统一。建一个 @my/tsconfig 包,导出共享的 base.json:
// packages/tsconfig/base.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true
}
}各子包只需 extends 它,规则全仓一致:
// packages/web/tsconfig.json
{
"extends": "@my/tsconfig/base.json",
"compilerOptions": { "outDir": "dist" }
}名词解释
类型单一来源(Single Source of Truth):同一个概念只定义一次,别处全部引用它。改一处全局生效,杜绝「复制十份类型、改了忘了同步」的漂移。
import type:TS 的「仅类型导入」语法,导入的东西运行时不存在,打包器可安全删除,避免误打包与循环依赖。共享类型包的标配写法。
tsconfig extends:子配置继承父配置(compilerOptions 合并),用来在 Monorepo 里统一 TS 编译规则。
Project References(项目引用):TS 的「包间依赖图」机制,子包用 references 声明依赖别的包,配合 tsc --build 实现「只重新编译变更及下游」的增量构建。大型 Monorepo 提速关键。
课后练习
练习 1:Pick<User, 'id' | 'name'> 和手写 { id: number; name: string } 哪个更好?
答案:用
Pick更好。它派生自User,User加了字段UserPreview自动跟着变;手写副本日后极易忘记同步,是类型漂移的温床。
练习 2:为什么从共享类型包导入要用 import type 而不是普通 import?
答案:
import type明确「这是类型、无运行时值」,打包器可安全丢弃、避免误打包,还能防止「类型循环引用」变成「运行时循环依赖」。语义也更清晰。
总结
Monorepo 的「复用」不是口号,而是三件具体的事:类型抽包、配置抽包、import type 只引类型。当 User 只在一处定义、所有包都 import type 引用它时,你才真正拥有了「改一处、全仓类型检查同步」的安全感。再配一个 @my/tsconfig 统一编译规则,团队的 TS 行为就不再是各写各的。下一节我们谈一个躲不开的坑——循环依赖,以及如何治理它。