共享包与内部依赖:类型/工具一处定义全仓复用

本节目标

Monorepo 最实在的收益,是「公共东西只定义一次」。典型共享包有三种:

  1. 类型包(如 @my/types):全仓共用的领域模型、API 响应类型。
  2. 工具包(如 @my/utils):格式化、校验等纯函数。
  3. 配置包(如 @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 行为就不再是各写各的。下一节我们谈一个躲不开的坑——循环依赖,以及如何治理它。