微前端架构下的 TypeScript 类型治理实践

日期 : 2026-02-10
标签 : TypeScript, 微前端, 架构治理, DX
阅读时间: 约 12 分钟


写在前面

你有没有遇到过这样的场景:

  • 打开一个 .ts 文件,IDE 疯狂报红 找不到名称"Recordable",但构建又能过?
  • 同一个 PageParam 接口,在 shims-vue.d.ts@cmclink/api@cmclink/types 三个地方各定义了一遍?
  • 想给一个类型加个字段,不知道该改哪个文件,改完发现另一个地方还是旧的?

这些都是我们 CMCLink 主应用在类型管理上踩过的坑。本文记录了一次完整的类型治理过程------从混乱到有序,从 10 个散落的类型文件到 3 个职责清晰的文件,以及背后的设计思想。


一、治理前:问题全景

1.1 types 目录的"垃圾抽屉"

治理前,apps/main/src/types/ 目录有 10 个文件,承担了远超其职责的工作:

css 复制代码
types/
├── auto-imports.d.ts    ← 自动生成,没问题
├── shims-vue.d.ts       ← 121 行!塞了 .vue 声明 + store 类型 + 50 行全局工具类型
├── container.d.ts       ← 249 行箱管类型,零引用(属于箱管子应用)
├── user.ts              ← 仅 store 和 1 个组件使用
├── flow.ts              ← 仅首页使用
├── msg.ts               ← 仅首页使用
├── marketing.ts         ← 仅首页使用
├── form.d.ts            ← Form 组件专属
├── components.d.ts      ← Form 组件专属
└── tabs.ts              ← 主应用路由/菜单类型

核心问题 :没有分类标准,什么类型都往 types/ 里扔。

1.2 全局类型的"三重影分身"

PageParam 这个接口,同时存在于:

typescript 复制代码
// 1️⃣ shims-vue.d.ts(全局隐式声明)
declare global {
  interface PageParam { pageSize?: number; pageNo?: number }
}

// 2️⃣ @cmclink/api/types.ts(公共包显式导出)
export interface PageParam { pageNo: number; pageSize: number }

// 3️⃣ @cmclink/types/global.d.ts(公共类型包全局声明)
declare global {
  interface PageParam { pageSize?: number; pageNo?: number }
}

三份定义,字段还有微妙差异(pageNo 是否可选)。改一处,另外两处不会同步。

1.3 公共类型包的"空城计"

@cmclink/types 包已经创建好了,README 写得很漂亮,9 个模块文件一应俱全。但是:

bash 复制代码
# 搜索整个 monorepo,谁在用 @cmclink/types?
$ grep -r "@cmclink/types" --include="*.ts" --include="*.vue" .
# 结果:0 条

零消费者。包搭好了,没人接入。


二、设计思想

2.1 类型归属三问

每遇到一个类型定义,问三个问题:

bash 复制代码
Q1: 几个子应用在用?
    ├── ≥2 个 → 放公共包
    └── 1 个  → 放当前应用

Q2: 它跟什么绑定?
    ├── API 请求 → @cmclink/api
    ├── 工具函数 → @cmclink/utils
    └── 纯类型   → @cmclink/types

Q3: 在应用内,它属于谁?
    ├── 组件专属 → 就近放组件目录
    ├── Store 专属 → stores/types.ts
    └── 页面专属 → views/xxx/types.ts

这三个问题形成了一个决策树,任何类型都能找到唯一归属。

2.2 显式优于隐式

全局类型(declare global)的最大问题是来源不可追溯 。当你在代码里写 params: PageParam,IDE 和代码审查者都不知道这个 PageParam 从哪来。

我们的原则:

类型种类 策略 理由
工具类型(Recordable/Nullable 全局声明 ✅ 高频使用,类似 Promise/Record,显式导入反而增加噪音
业务类型(PageParam/TokenType 显式导入 ✅ 来源可追溯,IDE 跳转可用,重构安全
typescript 复制代码
// ✅ 工具类型:全局使用,无需导入
const data: Recordable = {}
const el: Nullable<HTMLElement> = null

// ✅ 业务类型:显式导入,来源清晰
import type { PageParam } from '@cmclink/api'
import type { FormSchema } from '@cmclink/types/form'

2.3 就近原则

类型应该离使用它的代码尽可能近:

bash 复制代码
❌ 远距离:types/flow.ts → views/home/index.vue(跨 3 层目录)
✅ 就近化:views/home/types.ts → views/home/index.vue(同目录)

好处:

  • 删除页面时类型跟着删,不会留下孤儿文件
  • 代码审查时一目了然,不用跳来跳去
  • IDE 自动补全更精准,因为作用域更小

2.4 单一来源(Single Source of Truth)

每个类型只在一个地方定义,其他地方 re-export:

typescript 复制代码
// 📍 唯一定义:packages/types/src/form.ts
export type FormSchema = { ... }

// 📍 re-export:apps/main/src/components/Form/src/types.ts
export type { FormSchema } from '@cmclink/types/form'

修改时只改一处,所有消费者自动同步。


三、实施过程

3.1 Phase 0:types 目录清理

操作 文件 效果
删除 container.d.ts (249 行) 零引用的箱管类型,不属于主应用
合并 user.tsstores/types.ts UserRoleType 仅 store 和 1 个组件使用
就近化 flow.ts/msg.ts/marketing.tsviews/home/types.ts 首页专属 UI 类型
合并 form.d.ts + components.d.tscomponents/Form/src/types.ts Form 组件专属

结果:10 个文件 → 3 个文件,删除 406 行。

3.2 Phase 1 (P0):接入 @cmclink/types

jsonc 复制代码
// tsconfig.app.json --- 引入公共全局类型
{
  "include": [
    "src/**/*",
    "../../packages/types/src/global.d.ts",   // 新增
    "../../packages/types/src/env.d.ts",       // 新增
    "../../packages/types/src/router.d.ts"     // 新增
  ]
}
typescript 复制代码
// shims-vue.d.ts --- 从 121 行瘦身到 18 行
// 删除整个 declare global 块,全局类型由 @cmclink/types 统一提供
declare module "*.vue" {
  import type { DefineComponent } from "vue";
  const component: DefineComponent<object, object, unknown>;
  export default component;
}

3.3 Phase 2 (P1):消除隐式依赖

typescript 复制代码
// ❌ 治理前:PageParam 从天上掉下来
export const getMessageLists = (params: PageParam) => { ... }

// ✅ 治理后:来源清晰
import type { PageParam } from '@cmclink/api'
export const getMessageLists = (params: PageParam) => { ... }

3.4 Phase 3 (P2):公共包升级 + API 类型规范化

Form 类型 :将 @cmclink/types/form 从精简版(75 行)升级为完整版(169 行),主应用改为 re-export(152 行 → 21 行)。

Profile 类型 :6 个内联 interface 从 API 文件提取到同目录 types.ts,API 文件只保留请求逻辑。


四、收益分析

4.1 可量化收益

指标 治理前 治理后 变化
types/ 文件数 10 3 -70%
types/ 总行数 ~630 ~120 -81%
shims-vue.d.ts 行数 121 18 -85%
重复类型定义 3 处(PageParam 等) 0 -100%
死文件/零引用 1 个(container.d.ts 249 行) 0 -100%
@cmclink/types 消费者 0 1(主应用) 从零到一
Form types.ts 行数 152(本地定义) 21(re-export) -86%
profile.ts 行数 84(混合) 34(纯请求) -60%

4.2 不可量化收益

  • IDE 体验提升 :全局类型由 tsconfig include 统一提供,Recordable/ComponentRef 等不再时有时无地报红
  • 重构安全性:类型单一来源,改一处全局生效,不会出现"改了 A 忘了 B"
  • 新人上手成本降低:类型归属有明确规则,不用猜"这个类型该放哪"
  • 代码审查效率:API 文件只有请求逻辑,类型定义在独立文件,职责分离
  • 跨子应用一致性 :其他子应用接入 @cmclink/types 只需 3 步,类型定义天然统一

4.3 为后续子应用铺路

现在任何新子应用接入公共类型只需:

bash 复制代码
# Step 1: 添加依赖
pnpm add -D @cmclink/types

# Step 2: tsconfig include 全局类型(复制 3 行)

# Step 3: 删除本地重复声明(如果有的话)

五、成本分析

5.1 一次性成本

项目 耗时 风险
types 目录清理 + 就近化 ~30 min 低(纯重构,不改逻辑)
接入 @cmclink/types + 删除重复声明 ~10 min 低(tsconfig + 删代码)
消除隐式全局类型 ~15 min 低(添加 import 语句)
Form 类型升级 + re-export ~30 min 中(公共包变更,需验证)
Profile 类型提取 ~15 min 低(纯提取,不改逻辑)
合计 ~100 min ---

5.2 持续成本

项目 成本 说明
新增类型时的决策 极低 按决策树走,30 秒内确定归属
公共包类型变更 改一处,所有消费者自动同步
代码审查 降低 类型和逻辑分离,审查更聚焦

5.3 风险与缓解

风险 缓解措施
公共包改类型影响多个子应用 公共包类型变更需 CR 审批 + 全量构建验证
IDE 全局类型偶尔不识别 tsconfig include 路径明确,重启 TS Server 即可
团队成员不知道新规范 本文 + 公共TS类型管理方案.md + 代码审查把关

六、规范速查卡

贴在工位上(或者 bookmark 这篇文章):

bash 复制代码
┌─────────────────────────────────────────────┐
│           类型放哪里?速查表                   │
├─────────────────────────────────────────────┤
│                                              │
│  ≥2 个子应用用?                              │
│    ├── 跟 API 绑定 → @cmclink/api            │
│    ├── 跟工具函数绑定 → @cmclink/utils        │
│    └── 纯类型 → @cmclink/types               │
│                                              │
│  仅 1 个子应用用?                            │
│    ├── 组件专属 → 组件目录/types.ts           │
│    ├── Store 专属 → stores/types.ts          │
│    ├── 页面专属 → views/xxx/types.ts         │
│    └── API 专属 → api/xxx/types.ts           │
│                                              │
│  全局工具类型(Recordable 等)?               │
│    → @cmclink/types/global.d.ts              │
│    → tsconfig include 引入,无需 import       │
│                                              │
├─────────────────────────────────────────────┤
│  ❌ 禁止:                                    │
│  • shims-vue.d.ts 里加业务类型               │
│  • API 文件里内联 interface                  │
│  • 跨子应用直接 import 类型                   │
│  • @cmclink/types 里引入运行时依赖            │
└─────────────────────────────────────────────┘

七、总结

这次类型治理的本质是建立秩序。代码库像一座城市,类型定义是城市的地址系统。当地址系统混乱时,每个人都在花时间找路;当地址系统清晰时,所有人都能快速到达目的地。

三个核心原则:

  1. 单一来源 --- 每个类型只定义一次
  2. 显式优于隐式 --- 业务类型必须 import,来源可追溯
  3. 就近原则 --- 类型离使用它的代码越近越好

100 分钟的投入,换来的是整个团队在类型管理上的长期效率提升。值得。


如有疑问,欢迎在 Code Review 中讨论,或直接找我聊。

相关推荐
mCell8 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell9 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭9 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清9 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木9 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076609 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声9 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易9 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得09 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
anOnion10 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计