[{"data":1,"prerenderedAt":2182},["ShallowReactive",2],{"article-/topics/typescript/typescript-monorepo-dependency-boundaries-path-alias-exports-cycles":3,"related-typescript":453,"content-query-OWiWV8P9Qy":1851},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":12,"image":18,"imageQuery":19,"pexelsPhotoId":20,"pexelsUrl":21,"featured":6,"readingTime":22,"body":23,"_type":447,"_id":448,"_source":449,"_file":450,"_stem":451,"_extension":452},"/topics/typescript/typescript-monorepo-dependency-boundaries-path-alias-exports-cycles","typescript",false,"","TypeScript Monorepo 依赖边界治理：路径别名、包导出与循环依赖怎么控","Monorepo 真正难的不是把代码放进 packages 目录，而是防止边界继续假装存在。本文从路径别名、包导出、循环依赖和层级约束出发，讲清 TypeScript Monorepo 的依赖边界治理方法。","2026-06-08","HTMLPAGE 团队",[13,14,15,16,17],"TypeScript","Monorepo","Dependency Boundaries","Path Alias","Circular Dependency","/images/articles/typescript-monorepo-dependency-boundaries-path-alias-exports-cycles-featured.jpg","monorepo code architecture developer laptop",574087,"https://www.pexels.com/photo/person-using-macbook-on-table-top-574087/",17,{"type":24,"children":25,"toc":436},"root",[26,43,48,55,60,72,77,86,91,97,118,129,142,148,153,173,178,196,201,207,212,230,235,241,261,266,291,296,301,337,342,347,352,357,394,399],{"type":27,"tag":28,"props":29,"children":30},"element","p",{},[31,34,41],{"type":32,"value":33},"text","很多团队以为把仓库改成 Monorepo，就等于自动获得了更清晰的架构边界。现实通常相反：目录拆开以后，如果导入规则和包导出没有跟上，边界反而更容易被误判。看起来大家都在 ",{"type":27,"tag":35,"props":36,"children":38},"code",{"className":37},[],[39],{"type":32,"value":40},"packages/",{"type":32,"value":42}," 里按模块开发，实际上应用层照样可以直连别的包内部文件，公共包也可能反过来依赖业务包，最后只是在更漂亮的目录结构里继续写大泥球。",{"type":27,"tag":28,"props":44,"children":45},{},[46],{"type":32,"value":47},"TypeScript 在 Monorepo 里的边界治理价值，重点不在“写路径别名更优雅”，而在于把依赖方向、可访问层级和循环依赖问题显式化。只要这些问题仍然靠团队自觉，仓库规模一上来，约定就会被现实一点点磨穿。",{"type":27,"tag":49,"props":50,"children":52},"h2",{"id":51},"路径别名可以改善可读性但它不是边界本身",[53],{"type":32,"value":54},"路径别名可以改善可读性，但它不是边界本身",{"type":27,"tag":28,"props":56,"children":57},{},[58],{"type":32,"value":59},"路径别名最直接的好处当然是避免这类导入：",{"type":27,"tag":61,"props":62,"children":67},"pre",{"className":63,"code":65,"language":66,"meta":7},[64],"language-ts","import { formatMoney } from '../../../../shared/src/utils/formatMoney'\n","ts",[68],{"type":27,"tag":35,"props":69,"children":70},{"__ignoreMap":7},[71],{"type":32,"value":65},{"type":27,"tag":28,"props":73,"children":74},{},[75],{"type":32,"value":76},"改成统一别名后，代码看起来确实更干净：",{"type":27,"tag":61,"props":78,"children":81},{"className":79,"code":80,"language":66,"meta":7},[64],"import { formatMoney } from '@shared/utils/formatMoney'\n",[82],{"type":27,"tag":35,"props":83,"children":84},{"__ignoreMap":7},[85],{"type":32,"value":80},{"type":27,"tag":28,"props":87,"children":88},{},[89],{"type":32,"value":90},"问题在于，这样只是把“深层穿透”写得更像正常行为。如果别名直接指向另一个包的内部源码目录，你只是把问题包装得更好看了。真正该问的是：这个导入是否应该存在？它是不是应该只能通过包的公共入口访问？",{"type":27,"tag":49,"props":92,"children":94},{"id":93},"包导出决定了别人能看到什么它比-alias-更接近真实边界",[95],{"type":32,"value":96},"包导出决定了“别人能看到什么”，它比 alias 更接近真实边界",{"type":27,"tag":28,"props":98,"children":99},{},[100,102,108,110,116],{"type":32,"value":101},"在 Monorepo 里，",{"type":27,"tag":35,"props":103,"children":105},{"className":104},[],[106],{"type":32,"value":107},"package.json",{"type":32,"value":109}," 的 ",{"type":27,"tag":35,"props":111,"children":113},{"className":112},[],[114],{"type":32,"value":115},"exports",{"type":32,"value":117}," 才是更接近边界事实的工具。因为它不是在改善写法，而是在定义允许被消费的表面。",{"type":27,"tag":61,"props":119,"children":124},{"className":120,"code":122,"language":123,"meta":7},[121],"language-json","{\n  \"name\": \"@acme/shared\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    }\n  }\n}\n","json",[125],{"type":27,"tag":35,"props":126,"children":127},{"__ignoreMap":7},[128],{"type":32,"value":122},{"type":27,"tag":28,"props":130,"children":131},{},[132,134,140],{"type":32,"value":133},"一旦公共包只导出入口文件，下游再想去 import ",{"type":27,"tag":35,"props":135,"children":137},{"className":136},[],[138],{"type":32,"value":139},"src/internal",{"type":32,"value":141},"，就不会只是“代码风格不佳”，而会直接碰到机制约束。这正是边界治理需要的效果：让错误路径变难，而不是继续靠 code review 提醒。",{"type":27,"tag":49,"props":143,"children":145},{"id":144},"循环依赖不是等构建报错再说而是结构已经开始反咬你",[146],{"type":32,"value":147},"循环依赖不是“等构建报错再说”，而是结构已经开始反咬你",{"type":27,"tag":28,"props":149,"children":150},{},[151],{"type":32,"value":152},"循环依赖在 Monorepo 里尤其危险，因为它不一定马上导致构建失败。更常见的情况是：",{"type":27,"tag":154,"props":155,"children":156},"ul",{},[157,163,168],{"type":27,"tag":158,"props":159,"children":160},"li",{},[161],{"type":32,"value":162},"类型推导突然变慢",{"type":27,"tag":158,"props":164,"children":165},{},[166],{"type":32,"value":167},"某些运行时导入顺序出现诡异问题",{"type":27,"tag":158,"props":169,"children":170},{},[171],{"type":32,"value":172},"公共包越来越难抽离或单独测试",{"type":27,"tag":28,"props":174,"children":175},{},[176],{"type":32,"value":177},"典型路径是这样的：",{"type":27,"tag":154,"props":179,"children":180},{},[181,186,191],{"type":27,"tag":158,"props":182,"children":183},{},[184],{"type":32,"value":185},"业务包 A 依赖公共包 B 的工具函数。",{"type":27,"tag":158,"props":187,"children":188},{},[189],{"type":32,"value":190},"公共包 B 后来又顺手引用了 A 里的某个业务常量或类型。",{"type":27,"tag":158,"props":192,"children":193},{},[194],{"type":32,"value":195},"仓库一开始还能跑，时间久了依赖方向就彻底乱掉。",{"type":27,"tag":28,"props":197,"children":198},{},[199],{"type":32,"value":200},"只要公共层开始反向引用业务层，说明架构层次已经被打穿。循环依赖真正可怕的地方，不是 import 图不好看，而是所有“高层依赖低层”的假设都失效了。",{"type":27,"tag":49,"props":202,"children":204},{"id":203},"更稳的做法先定义层再定义允许的依赖方向",[205],{"type":32,"value":206},"更稳的做法：先定义层，再定义允许的依赖方向",{"type":27,"tag":28,"props":208,"children":209},{},[210],{"type":32,"value":211},"很多 Monorepo 治理失败，是因为团队先有 packages，再慢慢猜这些包应该处在哪一层。更稳的方式通常是先抽象层级，例如：",{"type":27,"tag":154,"props":213,"children":214},{},[215,220,225],{"type":27,"tag":158,"props":216,"children":217},{},[218],{"type":32,"value":219},"基础层：纯工具、类型、schema",{"type":27,"tag":158,"props":221,"children":222},{},[223],{"type":32,"value":224},"平台层：UI 组件、SDK、公共服务",{"type":27,"tag":158,"props":226,"children":227},{},[228],{"type":32,"value":229},"业务层：领域逻辑、页面、应用",{"type":27,"tag":28,"props":231,"children":232},{},[233],{"type":32,"value":234},"然后再决定依赖方向：业务层可以向下依赖平台层和基础层，平台层可以依赖基础层，基础层不能向上反依赖。只要这个方向没有先讲清，后面 alias、引用图和 lint 规则都只能算补救。",{"type":27,"tag":49,"props":236,"children":238},{"id":237},"一个常见失败案例共享包看起来最公共实际塞满业务特例",[239],{"type":32,"value":240},"一个常见失败案例：共享包看起来最公共，实际塞满业务特例",{"type":27,"tag":28,"props":242,"children":243},{},[244,246,252,254,259],{"type":32,"value":245},"某团队的 ",{"type":27,"tag":35,"props":247,"children":249},{"className":248},[],[250],{"type":32,"value":251},"shared",{"type":32,"value":253}," 包一开始只有日期、字符串和请求工具，后来为了图方便，逐渐塞进了营销活动常量、某个页面特有的表单字段映射，甚至某个业务模块的枚举。这样做短期很快，长期却让 ",{"type":27,"tag":35,"props":255,"children":257},{"className":256},[],[258],{"type":32,"value":251},{"type":32,"value":260}," 变成“谁都能往里塞，但没人敢拆”的黑洞。",{"type":27,"tag":28,"props":262,"children":263},{},[264],{"type":32,"value":265},"最后仓库里出现两个后果：",{"type":27,"tag":154,"props":267,"children":268},{},[269,281],{"type":27,"tag":158,"props":270,"children":271},{},[272,274,279],{"type":32,"value":273},"几乎所有包都依赖 ",{"type":27,"tag":35,"props":275,"children":277},{"className":276},[],[278],{"type":32,"value":251},{"type":32,"value":280},"，任何改动都有大面积影响。",{"type":27,"tag":158,"props":282,"children":283},{},[284,289],{"type":27,"tag":35,"props":285,"children":287},{"className":286},[],[288],{"type":32,"value":251},{"type":32,"value":290}," 内部其实又反向依赖某些业务包，形成隐性环。",{"type":27,"tag":28,"props":292,"children":293},{},[294],{"type":32,"value":295},"这类问题表面是依赖图混乱，根因其实是没有明确公共层的准入标准。不是所有“多个地方都能用到”的东西都该进 shared，只有稳定、通用、不会携带业务特例的能力才值得放进去。",{"type":27,"tag":49,"props":297,"children":299},{"id":298},"可执行的边界治理动作",[300],{"type":32,"value":298},{"type":27,"tag":302,"props":303,"children":304},"ol",{},[305,310,322,327,332],{"type":27,"tag":158,"props":306,"children":307},{},[308],{"type":32,"value":309},"先明确每个包属于哪一层，而不是只看目录名字。",{"type":27,"tag":158,"props":311,"children":312},{},[313,315,320],{"type":32,"value":314},"用 ",{"type":27,"tag":35,"props":316,"children":318},{"className":317},[],[319],{"type":32,"value":115},{"type":32,"value":321}," 收窄公共包的可见入口。",{"type":27,"tag":158,"props":323,"children":324},{},[325],{"type":32,"value":326},"把路径别名指向公共入口，而不是内部源码目录。",{"type":27,"tag":158,"props":328,"children":329},{},[330],{"type":32,"value":331},"用 lint、图分析或 CI 检查循环依赖和越层依赖。",{"type":27,"tag":158,"props":333,"children":334},{},[335],{"type":32,"value":336},"定期审视 shared 包是否开始吸收业务特例。",{"type":27,"tag":28,"props":338,"children":339},{},[340],{"type":32,"value":341},"边界治理最怕的不是规则少，而是规则只停在文档里。只要这些动作没进入自动化流程，仓库规模一大，例外就会持续积累。",{"type":27,"tag":49,"props":343,"children":345},{"id":344},"总结",[346],{"type":32,"value":344},{"type":27,"tag":28,"props":348,"children":349},{},[350],{"type":32,"value":351},"TypeScript Monorepo 的真正挑战，不是把 import 写短，而是把依赖方向做实。路径别名改善阅读体验，包导出定义可见表面，层级约束阻止向上反依赖，循环依赖检查负责尽早暴露结构问题。四件事缺一不可，否则 Monorepo 只是把大泥球切成了多个文件夹而已。",{"type":27,"tag":28,"props":353,"children":354},{},[355],{"type":32,"value":356},"本系列导航：",{"type":27,"tag":154,"props":358,"children":359},{},[360,372,383],{"type":27,"tag":158,"props":361,"children":362},{},[363,365],{"type":32,"value":364},"如果你还没把编译边界拆开，先回头读 ",{"type":27,"tag":366,"props":367,"children":369},"a",{"href":368},"/topics/typescript/typescript-project-references-tsconfig-layering-incremental-build-boundaries",[370],{"type":32,"value":371},"TypeScript 项目引用与 tsconfig 分层",{"type":27,"tag":158,"props":373,"children":374},{},[375,377],{"type":32,"value":376},"若你已经在治理依赖图，再接着看 ",{"type":27,"tag":366,"props":378,"children":380},{"href":379},"/topics/typescript/typescript-typecheck-performance-optimization-large-repo-playbook",[381],{"type":32,"value":382},"TypeScript 类型检查性能优化",{"type":27,"tag":158,"props":384,"children":385},{},[386,388],{"type":32,"value":387},"如果你还要继续收口对外契约边界，可读 ",{"type":27,"tag":366,"props":389,"children":391},{"href":390},"/topics/typescript/typescript-public-api-design-exported-types-breaking-change-control",[392],{"type":32,"value":393},"TypeScript 公共 API 设计",{"type":27,"tag":28,"props":395,"children":396},{},[397],{"type":32,"value":398},"延伸阅读：",{"type":27,"tag":154,"props":400,"children":401},{},[402,409,418,427],{"type":27,"tag":158,"props":403,"children":404},{},[405],{"type":27,"tag":366,"props":406,"children":407},{"href":368},[408],{"type":32,"value":371},{"type":27,"tag":158,"props":410,"children":411},{},[412],{"type":27,"tag":366,"props":413,"children":415},{"href":414},"/topics/frontend/monorepo-engineering-playbook",[416],{"type":32,"value":417},"Monorepo 工程化完整方案",{"type":27,"tag":158,"props":419,"children":420},{},[421],{"type":27,"tag":366,"props":422,"children":424},{"href":423},"/topics/frontend/pnpm-workspace-advanced-guide",[425],{"type":32,"value":426},"pnpm workspace 进阶技巧",{"type":27,"tag":158,"props":428,"children":429},{},[430],{"type":27,"tag":366,"props":431,"children":433},{"href":432},"/topics/frontend/typescript-build-tools-deep-integration",[434],{"type":32,"value":435},"TypeScript 与构建工具深度集成",{"title":7,"searchDepth":437,"depth":437,"links":438},3,[439,441,442,443,444,445,446],{"id":51,"depth":440,"text":54},2,{"id":93,"depth":440,"text":96},{"id":144,"depth":440,"text":147},{"id":203,"depth":440,"text":206},{"id":237,"depth":440,"text":240},{"id":298,"depth":440,"text":298},{"id":344,"depth":440,"text":344},"markdown","content:topics:typescript:typescript-monorepo-dependency-boundaries-path-alias-exports-cycles.md","content","topics/typescript/typescript-monorepo-dependency-boundaries-path-alias-exports-cycles.md","topics/typescript/typescript-monorepo-dependency-boundaries-path-alias-exports-cycles","md",[454,898,1319],{"_path":455,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":456,"description":457,"date":458,"topic":5,"author":11,"tags":459,"image":464,"featured":465,"readingTime":466,"body":467,"_type":447,"_id":895,"_source":449,"_file":896,"_stem":897,"_extension":452},"/topics/typescript/typescript-vue3-best-practices","TypeScript 在 Vue 3 中的最佳实践","深度讲解如何在 Vue 3 中高效使用 TypeScript，包括类型定义、接口设计、generics 应用、常见错误等完整指南。","2025-12-27",[13,460,461,462,463],"Vue 3","类型安全","最佳实践","接口设计","/images/topics/typescript-vue3.jpg",true,12,{"type":24,"children":468,"toc":868},[469,474,479,485,492,502,508,517,523,528,537,542,551,556,565,571,577,586,592,601,607,613,622,628,637,643,652,658,667,673,682,688,697,703,712,716,721,829,834],{"type":27,"tag":49,"props":470,"children":472},{"id":471},"typescript-在-vue-3-中的最佳实践",[473],{"type":32,"value":456},{"type":27,"tag":28,"props":475,"children":476},{},[477],{"type":32,"value":478},"TypeScript 让 Vue 开发更加安全可靠。本文讲解如何在 Vue 3 中高效使用 TypeScript。",{"type":27,"tag":49,"props":480,"children":482},{"id":481},"_1-基础类型定义",[483],{"type":32,"value":484},"1. 基础类型定义",{"type":27,"tag":486,"props":487,"children":489},"h3",{"id":488},"组件-props-的类型定义",[490],{"type":32,"value":491},"组件 Props 的类型定义",{"type":27,"tag":61,"props":493,"children":497},{"className":494,"code":496,"language":5,"meta":7},[495],"language-typescript","// ✅ 完整的类型定义\n\nimport { PropType } from 'vue'\n\ninterface User {\n  id: string\n  name: string\n  email: string\n  role: 'admin' | 'user' | 'guest'\n}\n\ninterface Props {\n  // 必需的属性\n  title: string\n  \n  // 可选属性\n  count?: number\n  \n  // 对象类型\n  user?: User\n  \n  // 数组类型\n  items?: (string | number)[]\n  \n  // 函数类型\n  onSubmit?: (data: any) => void\n  \n  // 字面量类型\n  size?: 'sm' | 'md' | 'lg'\n  \n  // any 类型 (避免)\n  // data?: any\n}\n\nexport default {\n  props: {\n    title: {\n      type: String,\n      required: true\n    },\n    \n    count: {\n      type: Number,\n      default: 0\n    },\n    \n    user: {\n      type: Object as PropType\u003CUser>,\n      default: () => ({})\n    },\n    \n    size: {\n      type: String,\n      default: 'md',\n      validator: (value: string) => ['sm', 'md', 'lg'].includes(value)\n    }\n  }\n}\n\n// 在 \u003Cscript setup> 中\n\u003Cscript setup lang=\"ts\">\ninterface Props {\n  title: string\n  count?: number\n}\n\nwithDefaults(defineProps\u003CProps>(), {\n  count: 0\n})\n\u003C/script>\n",[498],{"type":27,"tag":35,"props":499,"children":500},{"__ignoreMap":7},[501],{"type":32,"value":496},{"type":27,"tag":486,"props":503,"children":505},{"id":504},"组件-emits-的类型定义",[506],{"type":32,"value":507},"组件 Emits 的类型定义",{"type":27,"tag":61,"props":509,"children":512},{"className":510,"code":511,"language":5,"meta":7},[495],"// ✅ 类型安全的事件发射\n\ninterface Emits {\n  (e: 'submit', data: { name: string; email: string }): void\n  (e: 'cancel'): void\n  (e: 'delete', id: string): void\n}\n\n// 选项式 API\nexport default {\n  emits: {\n    submit: (data: { name: string; email: string }) => {\n      // 可选: 验证\n      return data.name && data.email\n    },\n    cancel: null,\n    delete: (id: string) => id !== ''\n  }\n}\n\n// \u003Cscript setup>\n\u003Cscript setup lang=\"ts\">\nconst emit = defineEmits\u003C{\n  submit: [data: { name: string; email: string }]\n  cancel: []\n  delete: [id: string]\n}>()\n\nconst handleSubmit = () => {\n  emit('submit', { name: 'Alice', email: 'alice@example.com' })\n}\n\u003C/script>\n",[513],{"type":27,"tag":35,"props":514,"children":515},{"__ignoreMap":7},[516],{"type":32,"value":511},{"type":27,"tag":49,"props":518,"children":520},{"id":519},"_2-高级类型模式",[521],{"type":32,"value":522},"2. 高级类型模式",{"type":27,"tag":486,"props":524,"children":526},{"id":525},"泛型组件",[527],{"type":32,"value":525},{"type":27,"tag":61,"props":529,"children":532},{"className":530,"code":531,"language":5,"meta":7},[495],"// components/List.vue\n\u003Cscript setup lang=\"ts\" generic=\"T extends Record\u003Cstring, any>\">\ninterface Props {\n  items: T[]\n  keyField?: keyof T\n}\n\nconst props = withDefaults(defineProps\u003CProps>(), {\n  keyField: 'id' as keyof T\n})\n\nconst emit = defineEmits\u003C{\n  select: [item: T]\n}>()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cdiv\n      v-for=\"item in items\"\n      :key=\"item[keyField]\"\n      @click=\"emit('select', item)\"\n    >\n      {{ item }}\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n// 使用泛型组件\n\u003Cscript setup lang=\"ts\">\ninterface User {\n  id: string\n  name: string\n}\n\nconst users = ref\u003CUser[]>([\n  { id: '1', name: 'Alice' },\n  { id: '2', name: 'Bob' }\n])\n\nconst handleSelect = (user: User) => {\n  console.log('选中:', user.name)\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- ✅ 类型完全推断 -->\n  \u003CList\n    :items=\"users\"\n    key-field=\"id\"\n    @select=\"handleSelect\"\n  />\n\u003C/template>\n",[533],{"type":27,"tag":35,"props":534,"children":535},{"__ignoreMap":7},[536],{"type":32,"value":531},{"type":27,"tag":486,"props":538,"children":540},{"id":539},"条件类型和分布式条件类型",[541],{"type":32,"value":539},{"type":27,"tag":61,"props":543,"children":546},{"className":544,"code":545,"language":5,"meta":7},[495],"// 条件类型 (Conditional Types)\n\n// 基础条件类型\ntype IsString\u003CT> = T extends string ? true : false\n\ntype A = IsString\u003C'hello'>  // true\ntype B = IsString\u003Cnumber>   // false\n\n// 分布式条件类型\ntype Flatten\u003CT> = T extends Array\u003Cinfer U> ? U : T\n\ntype Str = Flatten\u003Cstring[]>        // string\ntype Num = Flatten\u003Cnumber>          // number\ntype Mixed = Flatten\u003C(string | number)[]>  // string | number\n\n// 实战: 提取 Promise 的结果类型\ntype Awaited\u003CT> = T extends Promise\u003Cinfer U> ? U : T\n\ntype Result = Awaited\u003CPromise\u003Cstring>>  // string\n\n// 实战: API 响应类型\ninterface APIResponse\u003CT> {\n  code: number\n  message: string\n  data: T\n}\n\ntype ExtractData\u003CT> = T extends APIResponse\u003Cinfer D> ? D : never\n\ntype UserData = ExtractData\u003CAPIResponse\u003C{ name: string }>>  // { name: string }\n",[547],{"type":27,"tag":35,"props":548,"children":549},{"__ignoreMap":7},[550],{"type":32,"value":545},{"type":27,"tag":486,"props":552,"children":554},{"id":553},"复杂的接口设计",[555],{"type":32,"value":553},{"type":27,"tag":61,"props":557,"children":560},{"className":558,"code":559,"language":5,"meta":7},[495],"// 实战: 表单验证框架\n\n// 1. 定义字段验证规则\ninterface FieldRule {\n  required?: boolean\n  minLength?: number\n  maxLength?: number\n  pattern?: RegExp\n  custom?: (value: any) => boolean | string\n}\n\n// 2. 为每个字段定义规则\ninterface FormSchema {\n  [fieldName: string]: FieldRule\n}\n\n// 3. 带验证的表单处理\nclass FormValidator\u003CT extends Record\u003Cstring, any>> {\n  constructor(private schema: FormSchema) {}\n  \n  validate(data: T): Record\u003Ckeyof T, string[]> {\n    const errors: Record\u003Cstring, string[]> = {}\n    \n    for (const [field, rules] of Object.entries(this.schema)) {\n      const errors_list: string[] = []\n      const value = data[field]\n      \n      if (rules.required && !value) {\n        errors_list.push(`${field} 是必需的`)\n      }\n      \n      if (rules.minLength && value?.length \u003C rules.minLength) {\n        errors_list.push(`${field} 至少需要 ${rules.minLength} 个字符`)\n      }\n      \n      if (errors_list.length > 0) {\n        errors[field] = errors_list\n      }\n    }\n    \n    return errors as Record\u003Ckeyof T, string[]>\n  }\n}\n\n// 使用\ninterface LoginForm {\n  email: string\n  password: string\n}\n\nconst validator = new FormValidator\u003CLoginForm>({\n  email: { required: true, pattern: /^.+@.+\\..+$/ },\n  password: { required: true, minLength: 6 }\n})\n\nconst errors = validator.validate({\n  email: 'invalid',\n  password: '123'\n})\n",[561],{"type":27,"tag":35,"props":562,"children":563},{"__ignoreMap":7},[564],{"type":32,"value":559},{"type":27,"tag":49,"props":566,"children":568},{"id":567},"_3-组合式-api-的类型定义",[569],{"type":32,"value":570},"3. 组合式 API 的类型定义",{"type":27,"tag":486,"props":572,"children":574},{"id":573},"composable-的返回类型",[575],{"type":32,"value":576},"Composable 的返回类型",{"type":27,"tag":61,"props":578,"children":581},{"className":579,"code":580,"language":5,"meta":7},[495],"// composables/useCounter.ts\n\nimport { ref, computed, Ref } from 'vue'\n\n// 定义返回类型\ninterface UseCounterReturn {\n  count: Ref\u003Cnumber>\n  double: ComputedRef\u003Cnumber>\n  increment: () => void\n  reset: () => void\n}\n\n// 实现 composable\nexport const useCounter = (initialValue: number = 0): UseCounterReturn => {\n  const count = ref(initialValue)\n  \n  const double = computed(() => count.value * 2)\n  \n  const increment = () => count.value++\n  \n  const reset = () => count.value = initialValue\n  \n  return {\n    count,\n    double,\n    increment,\n    reset\n  }\n}\n\n// 使用时自动推断类型\n\u003Cscript setup lang=\"ts\">\nconst { count, double, increment } = useCounter(10)\n// count: Ref\u003Cnumber>\n// double: ComputedRef\u003Cnumber>\n// increment: () => void\n\u003C/script>\n",[582],{"type":27,"tag":35,"props":583,"children":584},{"__ignoreMap":7},[585],{"type":32,"value":580},{"type":27,"tag":486,"props":587,"children":589},{"id":588},"composable-的泛型",[590],{"type":32,"value":591},"Composable 的泛型",{"type":27,"tag":61,"props":593,"children":596},{"className":594,"code":595,"language":5,"meta":7},[495],"// composables/useFetch.ts\n\ninterface UseFetchReturn\u003CT> {\n  data: Ref\u003CT | null>\n  loading: Ref\u003Cboolean>\n  error: Ref\u003CError | null>\n  refresh: () => Promise\u003Cvoid>\n}\n\nexport const useFetch = async \u003CT = any>(\n  url: string | Ref\u003Cstring>,\n  options?: FetchOptions\n): Promise\u003CUseFetchReturn\u003CT>> => {\n  const data = ref\u003CT | null>(null)\n  const loading = ref(false)\n  const error = ref\u003CError | null>(null)\n  \n  const refresh = async () => {\n    loading.value = true\n    try {\n      const response = await $fetch\u003CT>(url, options)\n      data.value = response\n    } catch (e) {\n      error.value = e as Error\n    } finally {\n      loading.value = false\n    }\n  }\n  \n  await refresh()\n  \n  return { data, loading, error, refresh }\n}\n\n// 使用\n\u003Cscript setup lang=\"ts\">\ninterface User {\n  id: string\n  name: string\n  email: string\n}\n\nconst { data: users, loading } = await useFetch\u003CUser[]>('/api/users')\n// users: Ref\u003CUser[] | null>\n// loading: Ref\u003Cboolean>\n\u003C/script>\n",[597],{"type":27,"tag":35,"props":598,"children":599},{"__ignoreMap":7},[600],{"type":32,"value":595},{"type":27,"tag":49,"props":602,"children":604},{"id":603},"_4-常见类型错误和解决方案",[605],{"type":32,"value":606},"4. 常见类型错误和解决方案",{"type":27,"tag":486,"props":608,"children":610},{"id":609},"错误-1-any-类型滥用",[611],{"type":32,"value":612},"错误 1: Any 类型滥用",{"type":27,"tag":61,"props":614,"children":617},{"className":615,"code":616,"language":5,"meta":7},[495],"// ❌ 避免\nconst handleClick = (event: any) => {\n  console.log(event.target.value)  // 无类型检查\n}\n\nconst fetchData = async (url: any) => {\n  const response = await $fetch(url)\n  return response  // any 类型\n}\n\n// ✅ 正确\nconst handleClick = (event: MouseEvent) => {\n  if (event.target instanceof HTMLInputElement) {\n    console.log(event.target.value)\n  }\n}\n\ninterface FetchOptions {\n  method?: 'GET' | 'POST'\n  headers?: Record\u003Cstring, string>\n  body?: Record\u003Cstring, any>\n}\n\nconst fetchData = async (url: string, options?: FetchOptions) => {\n  const response = await $fetch(url, options)\n  return response\n}\n",[618],{"type":27,"tag":35,"props":619,"children":620},{"__ignoreMap":7},[621],{"type":32,"value":616},{"type":27,"tag":486,"props":623,"children":625},{"id":624},"错误-2-类型断言滥用",[626],{"type":32,"value":627},"错误 2: 类型断言滥用",{"type":27,"tag":61,"props":629,"children":632},{"className":630,"code":631,"language":5,"meta":7},[495],"// ❌ 避免 (类型断言隐藏问题)\nconst data = response as User[]\nconst count = element as HTMLInputElement\n\n// ✅ 正确 (类型保护)\nconst isUserArray = (data: unknown): data is User[] => {\n  return Array.isArray(data) && data.every(item => 'id' in item)\n}\n\nif (isUserArray(response)) {\n  // 现在 response 被确定为 User[]\n}\n\nconst parseElement = (element: Element): HTMLInputElement | null => {\n  if (element instanceof HTMLInputElement) {\n    return element\n  }\n  return null\n}\n",[633],{"type":27,"tag":35,"props":634,"children":635},{"__ignoreMap":7},[636],{"type":32,"value":631},{"type":27,"tag":486,"props":638,"children":640},{"id":639},"错误-3-props-类型和运行时定义不匹配",[641],{"type":32,"value":642},"错误 3: Props 类型和运行时定义不匹配",{"type":27,"tag":61,"props":644,"children":647},{"className":645,"code":646,"language":5,"meta":7},[495],"// ❌ 避免 (类型和运行时不一致)\ninterface Props {\n  user: User | null\n}\n\nexport default {\n  props: {\n    user: String  // ❌ 类型是 object，运行时是 string!\n  }\n}\n\n// ✅ 正确\ninterface Props {\n  user?: User | null\n}\n\nexport default {\n  props: {\n    user: {\n      type: Object as PropType\u003CUser | null>,\n      default: null\n    }\n  }\n}\n",[648],{"type":27,"tag":35,"props":649,"children":650},{"__ignoreMap":7},[651],{"type":32,"value":646},{"type":27,"tag":49,"props":653,"children":655},{"id":654},"_5-pinia-store-的类型定义",[656],{"type":32,"value":657},"5. Pinia Store 的类型定义",{"type":27,"tag":61,"props":659,"children":662},{"className":660,"code":661,"language":5,"meta":7},[495],"// stores/useUserStore.ts\n\ninterface User {\n  id: string\n  name: string\n  email: string\n  role: 'admin' | 'user'\n}\n\ninterface UserState {\n  users: User[]\n  currentUser: User | null\n  loading: boolean\n  error: string | null\n}\n\nexport const useUserStore = defineStore('user', {\n  state: (): UserState => ({\n    users: [],\n    currentUser: null,\n    loading: false,\n    error: null\n  }),\n  \n  getters: {\n    isAdmin: (state) => state.currentUser?.role === 'admin',\n    \n    getUserById: (state) => (id: string): User | undefined => {\n      return state.users.find(user => user.id === id)\n    }\n  },\n  \n  actions: {\n    async fetchUsers(): Promise\u003Cvoid> {\n      this.loading = true\n      try {\n        const users = await $fetch\u003CUser[]>('/api/users')\n        this.users = users\n      } catch (error: any) {\n        this.error = error.message\n      } finally {\n        this.loading = false\n      }\n    },\n    \n    setCurrentUser(user: User | null): void {\n      this.currentUser = user\n    }\n  }\n})\n\n// 使用\n\u003Cscript setup lang=\"ts\">\nconst userStore = useUserStore()\n\n// 所有都有完整的类型提示\nconst { currentUser, isAdmin } = storeToRefs(userStore)\nconst user = userStore.getUserById('123')  // User | undefined\n\u003C/script>\n",[663],{"type":27,"tag":35,"props":664,"children":665},{"__ignoreMap":7},[666],{"type":32,"value":661},{"type":27,"tag":49,"props":668,"children":670},{"id":669},"_6-api-响应的类型定义",[671],{"type":32,"value":672},"6. API 响应的类型定义",{"type":27,"tag":61,"props":674,"children":677},{"className":675,"code":676,"language":5,"meta":7},[495],"// types/api.ts\n\n// 通用 API 响应格式\ninterface APIResponse\u003CT> {\n  code: number\n  message: string\n  data: T\n  timestamp: number\n}\n\n// 分页响应\ninterface PaginatedResponse\u003CT> {\n  items: T[]\n  total: number\n  page: number\n  pageSize: number\n  hasMore: boolean\n}\n\n// 具体的 API 类型\n\ninterface User {\n  id: string\n  name: string\n  email: string\n  avatar?: string\n  createdAt: string\n}\n\ninterface UserResponse extends APIResponse\u003CUser> {}\n\ninterface UsersListResponse extends APIResponse\u003CPaginatedResponse\u003CUser>> {}\n\ntype CreateUserRequest = Omit\u003CUser, 'id' | 'createdAt'>\n\n// API 调用类型安全\n\u003Cscript setup lang=\"ts\">\nconst getUserList = async (): Promise\u003CUsersListResponse> => {\n  return await $fetch('/api/users')\n}\n\nconst createUser = async (data: CreateUserRequest): Promise\u003CUserResponse> => {\n  return await $fetch('/api/users', {\n    method: 'POST',\n    body: data\n  })\n}\n\n// 使用\nconst response = await getUserList()\n// response: UsersListResponse\n// response.data.items: User[]\n\u003C/script>\n",[678],{"type":27,"tag":35,"props":679,"children":680},{"__ignoreMap":7},[681],{"type":32,"value":676},{"type":27,"tag":49,"props":683,"children":685},{"id":684},"_7-类型工具函数",[686],{"type":32,"value":687},"7. 类型工具函数",{"type":27,"tag":61,"props":689,"children":692},{"className":690,"code":691,"language":5,"meta":7},[495],"// types/utils.ts\n\n// 1. 提取对象键的联合类型\ntype Keys\u003CT> = keyof T\ntype UserKeys = Keys\u003CUser>  // 'id' | 'name' | 'email'\n\n// 2. 提取对象值的联合类型\ntype Values\u003CT> = T[keyof T]\ntype UserValues = Values\u003CUser>  // string | number\n\n// 3. 部分可选\ntype PartialBy\u003CT, K extends keyof T> = Omit\u003CT, K> & Partial\u003CPick\u003CT, K>>\n\ntype UserWithOptionalEmail = PartialBy\u003CUser, 'email'>\n// { id: string; name: string; email?: string }\n\n// 4. 深度只读\ntype DeepReadonly\u003CT> = {\n  readonly [P in keyof T]: DeepReadonly\u003CT[P]>\n}\n\n// 5. 记录类型\ntype UserRole = 'admin' | 'user' | 'guest'\ntype RolePermissions = Record\u003CUserRole, string[]>\n\nconst permissions: RolePermissions = {\n  admin: ['read', 'write', 'delete'],\n  user: ['read', 'write'],\n  guest: ['read']\n}\n\n// 6. 排除类型\ntype Exclude\u003CT, U> = T extends U ? never : T\ntype Admin = Exclude\u003CUserRole, 'guest' | 'user'>  // 'admin'\n\n// 7. 提取类型\ntype Extract\u003CT, U> = T extends U ? T : never\ntype NotAdmin = Extract\u003CUserRole, Exclude\u003CUserRole, 'admin'>>  // 'user' | 'guest'\n",[693],{"type":27,"tag":35,"props":694,"children":695},{"__ignoreMap":7},[696],{"type":32,"value":691},{"type":27,"tag":49,"props":698,"children":700},{"id":699},"_8-最佳实践总结",[701],{"type":32,"value":702},"8. 最佳实践总结",{"type":27,"tag":61,"props":704,"children":707},{"className":705,"code":706,"language":5,"meta":7},[495],"// ✅ TypeScript 最佳实践清单\n\n// 1. 优先使用 interface 定义数据结构\ninterface User {\n  id: string\n  name: string\n}\n\n// 2. 为函数参数和返回值添加类型\nfunction getUser(id: string): Promise\u003CUser> {\n  // ...\n}\n\n// 3. 避免使用 any，使用 unknown 然后类型保护\n// ❌ const data: any\n// ✅ const data: unknown\nif (typeof data === 'object' && data !== null) {\n  // 现在可以安全地使用 data\n}\n\n// 4. 使用字面量类型替代字符串常量\n// ❌ status: string\n// ✅ status: 'pending' | 'success' | 'error'\n\n// 5. 充分利用泛型\nconst useData = \u003CT>(url: string): Promise\u003CT> => {\n  // ...\n}\n\n// 6. 为 Vue 组件的 Props 和 Emits 定义类型\ninterface Props {\n  title: string\n  count?: number\n}\n\ntype Emits = {\n  'update:title': [title: string]\n  'increment': []\n}\n",[708],{"type":27,"tag":35,"props":709,"children":710},{"__ignoreMap":7},[711],{"type":32,"value":706},{"type":27,"tag":49,"props":713,"children":714},{"id":344},[715],{"type":32,"value":344},{"type":27,"tag":28,"props":717,"children":718},{},[719],{"type":32,"value":720},"TypeScript 在 Vue 3 中的优势：",{"type":27,"tag":722,"props":723,"children":724},"table",{},[725,744],{"type":27,"tag":726,"props":727,"children":728},"thead",{},[729],{"type":27,"tag":730,"props":731,"children":732},"tr",{},[733,739],{"type":27,"tag":734,"props":735,"children":736},"th",{},[737],{"type":32,"value":738},"特性",{"type":27,"tag":734,"props":740,"children":741},{},[742],{"type":32,"value":743},"优势",{"type":27,"tag":745,"props":746,"children":747},"tbody",{},[748,765,781,797,813],{"type":27,"tag":730,"props":749,"children":750},{},[751,760],{"type":27,"tag":752,"props":753,"children":754},"td",{},[755],{"type":27,"tag":756,"props":757,"children":758},"strong",{},[759],{"type":32,"value":461},{"type":27,"tag":752,"props":761,"children":762},{},[763],{"type":32,"value":764},"编译时发现错误",{"type":27,"tag":730,"props":766,"children":767},{},[768,776],{"type":27,"tag":752,"props":769,"children":770},{},[771],{"type":27,"tag":756,"props":772,"children":773},{},[774],{"type":32,"value":775},"自动补全",{"type":27,"tag":752,"props":777,"children":778},{},[779],{"type":32,"value":780},"IDE 提示更准确",{"type":27,"tag":730,"props":782,"children":783},{},[784,792],{"type":27,"tag":752,"props":785,"children":786},{},[787],{"type":27,"tag":756,"props":788,"children":789},{},[790],{"type":32,"value":791},"可维护性",{"type":27,"tag":752,"props":793,"children":794},{},[795],{"type":32,"value":796},"代码意图更明确",{"type":27,"tag":730,"props":798,"children":799},{},[800,808],{"type":27,"tag":752,"props":801,"children":802},{},[803],{"type":27,"tag":756,"props":804,"children":805},{},[806],{"type":32,"value":807},"重构安全",{"type":27,"tag":752,"props":809,"children":810},{},[811],{"type":32,"value":812},"改变代码有反馈",{"type":27,"tag":730,"props":814,"children":815},{},[816,824],{"type":27,"tag":752,"props":817,"children":818},{},[819],{"type":27,"tag":756,"props":820,"children":821},{},[822],{"type":32,"value":823},"文档作用",{"type":27,"tag":752,"props":825,"children":826},{},[827],{"type":32,"value":828},"类型即文档",{"type":27,"tag":49,"props":830,"children":832},{"id":831},"相关资源",[833],{"type":32,"value":831},{"type":27,"tag":154,"props":835,"children":836},{},[837,848,858],{"type":27,"tag":158,"props":838,"children":839},{},[840],{"type":27,"tag":366,"props":841,"children":845},{"href":842,"rel":843},"https://www.typescriptlang.org/docs/",[844],"nofollow",[846],{"type":32,"value":847},"TypeScript 官方手册",{"type":27,"tag":158,"props":849,"children":850},{},[851],{"type":27,"tag":366,"props":852,"children":855},{"href":853,"rel":854},"https://vuejs.org/guide/typescript/overview.html",[844],[856],{"type":32,"value":857},"Vue 3 TypeScript 指南",{"type":27,"tag":158,"props":859,"children":860},{},[861],{"type":27,"tag":366,"props":862,"children":865},{"href":863,"rel":864},"https://www.typescriptlang.org/docs/handbook/2/types-from-types.html",[844],[866],{"type":32,"value":867},"TypeScript 高级类型",{"title":7,"searchDepth":437,"depth":437,"links":869},[870,871,875,880,884,889,890,891,892,893,894],{"id":471,"depth":440,"text":456},{"id":481,"depth":440,"text":484,"children":872},[873,874],{"id":488,"depth":437,"text":491},{"id":504,"depth":437,"text":507},{"id":519,"depth":440,"text":522,"children":876},[877,878,879],{"id":525,"depth":437,"text":525},{"id":539,"depth":437,"text":539},{"id":553,"depth":437,"text":553},{"id":567,"depth":440,"text":570,"children":881},[882,883],{"id":573,"depth":437,"text":576},{"id":588,"depth":437,"text":591},{"id":603,"depth":440,"text":606,"children":885},[886,887,888],{"id":609,"depth":437,"text":612},{"id":624,"depth":437,"text":627},{"id":639,"depth":437,"text":642},{"id":654,"depth":440,"text":657},{"id":669,"depth":440,"text":672},{"id":684,"depth":440,"text":687},{"id":699,"depth":440,"text":702},{"id":344,"depth":440,"text":344},{"id":831,"depth":440,"text":831},"content:topics:typescript:typescript-vue3-best-practices.md","topics/typescript/typescript-vue3-best-practices.md","topics/typescript/typescript-vue3-best-practices",{"_path":899,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":900,"description":901,"date":10,"topic":5,"author":11,"tags":902,"image":907,"imageQuery":908,"pexelsPhotoId":909,"pexelsUrl":910,"featured":6,"readingTime":22,"body":911,"_type":447,"_id":1316,"_source":449,"_file":1317,"_stem":1318,"_extension":452},"/topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy","TypeScript 设计模式实战：工厂、策略、适配器与代理怎样保持类型安全","设计模式在 TypeScript 里不该变成 class 套娃。本文从工厂、策略、适配器和代理四个高频场景出发，讲清如何让抽象保持灵活，同时不牺牲类型安全和可读性。",[13,903,904,905,906],"Design Patterns","Factory Pattern","Strategy Pattern","Adapter Pattern","/images/articles/typescript-design-patterns-factory-strategy-adapter-proxy-featured.jpg","software design patterns code laptop",34804023,"https://www.pexels.com/photo/close-up-of-computer-screen-with-code-reflection-34804023/",{"type":24,"children":912,"toc":1307},[913,918,931,937,942,951,964,970,975,984,997,1003,1024,1033,1038,1044,1049,1058,1063,1069,1074,1109,1114,1120,1219,1223,1228,1232,1266,1270],{"type":27,"tag":28,"props":914,"children":915},{},[916],{"type":32,"value":917},"很多人一提到“设计模式 + TypeScript”，脑子里马上冒出一套厚重的 class 结构：抽象工厂、基类、接口、子类、再来几层继承。问题在于，前端和 Node 项目里真正需要的，往往不是把经典 OO 图谱原样搬进来，而是把“变化点”抽象出来，同时保证调用方还能读懂类型。",{"type":27,"tag":28,"props":919,"children":920},{},[921,923,929],{"type":32,"value":922},"TypeScript 在设计模式里的价值，不是让模式更像 Java，而是让模式更诚实。哪些输入是合法的、哪些策略实现必须覆盖、适配器是否真的完成字段转换、代理是否保留原函数签名，这些都可以由类型系统提前约束。如果模式一上来就把类型信息打成 ",{"type":27,"tag":35,"props":924,"children":926},{"className":925},[],[927],{"type":32,"value":928},"any",{"type":32,"value":930},"，那它带来的通常不是灵活，而是延后的风险。",{"type":27,"tag":49,"props":932,"children":934},{"id":933},"工厂模式把怎么创建隔离出来但别把输入做成万能配置桶",[935],{"type":32,"value":936},"工厂模式：把“怎么创建”隔离出来，但别把输入做成万能配置桶",{"type":27,"tag":28,"props":938,"children":939},{},[940],{"type":32,"value":941},"工厂模式最适合解决创建逻辑和环境差异问题，比如：不同运行环境要创建不同客户端，不同支付方式要创建不同处理器。",{"type":27,"tag":61,"props":943,"children":946},{"className":944,"code":945,"language":66,"meta":7},[64],"type StorageKind = 'memory' | 'redis'\n\ninterface Storage {\n  get(key: string): Promise\u003Cstring | null>\n  set(key: string, value: string): Promise\u003Cvoid>\n}\n\nfunction createStorage(kind: StorageKind): Storage {\n  switch (kind) {\n    case 'memory':\n      return createMemoryStorage()\n    case 'redis':\n      return createRedisStorage()\n  }\n}\n",[947],{"type":27,"tag":35,"props":948,"children":949},{"__ignoreMap":7},[950],{"type":32,"value":945},{"type":27,"tag":28,"props":952,"children":953},{},[954,956,962],{"type":32,"value":955},"真正要避免的，不是工厂本身，而是把工厂入参做成一个什么都能塞的 ",{"type":27,"tag":35,"props":957,"children":959},{"className":958},[],[960],{"type":32,"value":961},"Record\u003Cstring, any>",{"type":32,"value":963},"。工厂模式一旦把输入边界放宽，调用方就会失去类型提示，后面连工厂是否真的支持某个组合都看不出来。",{"type":27,"tag":49,"props":965,"children":967},{"id":966},"策略模式别用-if-else-链拖着业务跑把变化面显式枚举出来",[968],{"type":32,"value":969},"策略模式：别用 if else 链拖着业务跑，把变化面显式枚举出来",{"type":27,"tag":28,"props":971,"children":972},{},[973],{"type":32,"value":974},"策略模式最值钱的地方，是把“可替换规则”显式列出来，让新增策略变成扩展而不是改旧逻辑。",{"type":27,"tag":61,"props":976,"children":979},{"className":977,"code":978,"language":66,"meta":7},[64],"type PricingStrategy = 'standard' | 'vip' | 'promotion'\n\nconst pricingMap: Record\u003CPricingStrategy, (price: number) => number> = {\n  standard: (price) => price,\n  vip: (price) => price * 0.9,\n  promotion: (price) => price - 30\n}\n\nfunction calcPrice(strategy: PricingStrategy, basePrice: number): number {\n  return pricingMap[strategy](basePrice)\n}\n",[980],{"type":27,"tag":35,"props":981,"children":982},{"__ignoreMap":7},[983],{"type":32,"value":978},{"type":27,"tag":28,"props":985,"children":986},{},[987,989,995],{"type":32,"value":988},"这里 ",{"type":27,"tag":35,"props":990,"children":992},{"className":991},[],[993],{"type":32,"value":994},"Record\u003CPricingStrategy, ...>",{"type":32,"value":996}," 的意义很大：只要你新增一个策略名，TypeScript 会强制你把实现补齐。这样策略模式不只是结构好看，而是具备穷举约束。",{"type":27,"tag":49,"props":998,"children":1000},{"id":999},"适配器模式真正重要的是把外部不稳定结构拦在边界外",[1001],{"type":32,"value":1002},"适配器模式：真正重要的是把外部不稳定结构拦在边界外",{"type":27,"tag":28,"props":1004,"children":1005},{},[1006,1008,1014,1016,1022],{"type":32,"value":1007},"适配器最常被低估。很多团队明明已经接了多个第三方服务，却还在业务层到处判断“这个平台字段叫 ",{"type":27,"tag":35,"props":1009,"children":1011},{"className":1010},[],[1012],{"type":32,"value":1013},"full_name",{"type":32,"value":1015},"，那个平台叫 ",{"type":27,"tag":35,"props":1017,"children":1019},{"className":1018},[],[1020],{"type":32,"value":1021},"displayName",{"type":32,"value":1023},"”。本质上，这就是缺了适配器层。",{"type":27,"tag":61,"props":1025,"children":1028},{"className":1026,"code":1027,"language":66,"meta":7},[64],"type VendorUser = {\n  uid: string\n  full_name: string\n  active_flag: 0 | 1\n}\n\ntype UserProfile = {\n  id: string\n  name: string\n  isActive: boolean\n}\n\nfunction adaptVendorUser(input: VendorUser): UserProfile {\n  return {\n    id: input.uid,\n    name: input.full_name,\n    isActive: input.active_flag === 1\n  }\n}\n",[1029],{"type":27,"tag":35,"props":1030,"children":1031},{"__ignoreMap":7},[1032],{"type":32,"value":1027},{"type":27,"tag":28,"props":1034,"children":1035},{},[1036],{"type":32,"value":1037},"适配器模式的关键，不是写一个转换函数，而是把外部世界的不稳定命名和字段结构限制在边界里，别让业务层长期直接面对这些差异。",{"type":27,"tag":49,"props":1039,"children":1041},{"id":1040},"代理模式包装额外行为时最怕把原始签名弄丢",[1042],{"type":32,"value":1043},"代理模式：包装额外行为时，最怕把原始签名弄丢",{"type":27,"tag":28,"props":1045,"children":1046},{},[1047],{"type":32,"value":1048},"代理模式常用于日志、缓存、权限检查和重试。它最常见的问题是：包装之后，原函数签名丢了，返回值也宽了，调用方只能面对一个模糊函数。",{"type":27,"tag":61,"props":1050,"children":1053},{"className":1051,"code":1052,"language":66,"meta":7},[64],"function withTiming\u003CTArgs extends unknown[], TResult>(\n  fn: (...args: TArgs) => Promise\u003CTResult>\n) {\n  return async (...args: TArgs): Promise\u003CTResult> => {\n    const start = performance.now()\n    try {\n      return await fn(...args)\n    } finally {\n      console.log('cost', performance.now() - start)\n    }\n  }\n}\n",[1054],{"type":27,"tag":35,"props":1055,"children":1056},{"__ignoreMap":7},[1057],{"type":32,"value":1052},{"type":27,"tag":28,"props":1059,"children":1060},{},[1061],{"type":32,"value":1062},"这类泛型代理的价值在于：你加了额外行为，但原始参数和返回值仍然保留下来。否则代理模式会把“附加控制”变成“类型信息丢失”。",{"type":27,"tag":49,"props":1064,"children":1066},{"id":1065},"一个常见失败案例模式是抽象了类型却退化成-any",[1067],{"type":32,"value":1068},"一个常见失败案例：模式是抽象了，类型却退化成 any",{"type":27,"tag":28,"props":1070,"children":1071},{},[1072],{"type":32,"value":1073},"很多项目在做所谓“模式升级”时，会出现一种反效果：结构更复杂了，类型却更差了。常见表现有：",{"type":27,"tag":154,"props":1075,"children":1076},{},[1077,1088,1093,1104],{"type":27,"tag":158,"props":1078,"children":1079},{},[1080,1082],{"type":32,"value":1081},"工厂接收 ",{"type":27,"tag":35,"props":1083,"children":1085},{"className":1084},[],[1086],{"type":32,"value":1087},"config: any",{"type":27,"tag":158,"props":1089,"children":1090},{},[1091],{"type":32,"value":1092},"策略表写成对象，但 key 没有联合类型约束",{"type":27,"tag":158,"props":1094,"children":1095},{},[1096,1098],{"type":32,"value":1097},"适配器只返回 ",{"type":27,"tag":35,"props":1099,"children":1101},{"className":1100},[],[1102],{"type":32,"value":1103},"Record\u003Cstring, unknown>",{"type":27,"tag":158,"props":1105,"children":1106},{},[1107],{"type":32,"value":1108},"代理函数包装后不保留原始签名",{"type":27,"tag":28,"props":1110,"children":1111},{},[1112],{"type":32,"value":1113},"这种代码看起来“更有架构感”，实际上却把很多风险从编译期挪回了运行时。模式如果不能提升边界清晰度和替换安全性，通常只是引入了新的复杂度。",{"type":27,"tag":49,"props":1115,"children":1117},{"id":1116},"决策表什么时候该上模式什么时候先别上",[1118],{"type":32,"value":1119},"决策表：什么时候该上模式，什么时候先别上",{"type":27,"tag":722,"props":1121,"children":1122},{},[1123,1144],{"type":27,"tag":726,"props":1124,"children":1125},{},[1126],{"type":27,"tag":730,"props":1127,"children":1128},{},[1129,1134,1139],{"type":27,"tag":734,"props":1130,"children":1131},{},[1132],{"type":32,"value":1133},"场景",{"type":27,"tag":734,"props":1135,"children":1136},{},[1137],{"type":32,"value":1138},"更适合什么模式",{"type":27,"tag":734,"props":1140,"children":1141},{},[1142],{"type":32,"value":1143},"不建议做的事",{"type":27,"tag":745,"props":1145,"children":1146},{},[1147,1165,1183,1201],{"type":27,"tag":730,"props":1148,"children":1149},{},[1150,1155,1160],{"type":27,"tag":752,"props":1151,"children":1152},{},[1153],{"type":32,"value":1154},"创建逻辑随环境变化",{"type":27,"tag":752,"props":1156,"children":1157},{},[1158],{"type":32,"value":1159},"工厂",{"type":27,"tag":752,"props":1161,"children":1162},{},[1163],{"type":32,"value":1164},"把所有可选项塞进一个大配置对象",{"type":27,"tag":730,"props":1166,"children":1167},{},[1168,1173,1178],{"type":27,"tag":752,"props":1169,"children":1170},{},[1171],{"type":32,"value":1172},"规则可替换、可扩展",{"type":27,"tag":752,"props":1174,"children":1175},{},[1176],{"type":32,"value":1177},"策略",{"type":27,"tag":752,"props":1179,"children":1180},{},[1181],{"type":32,"value":1182},"继续堆 if else 并靠注释解释",{"type":27,"tag":730,"props":1184,"children":1185},{},[1186,1191,1196],{"type":27,"tag":752,"props":1187,"children":1188},{},[1189],{"type":32,"value":1190},"第三方结构不稳定",{"type":27,"tag":752,"props":1192,"children":1193},{},[1194],{"type":32,"value":1195},"适配器",{"type":27,"tag":752,"props":1197,"children":1198},{},[1199],{"type":32,"value":1200},"让业务层到处处理字段差异",{"type":27,"tag":730,"props":1202,"children":1203},{},[1204,1209,1214],{"type":27,"tag":752,"props":1205,"children":1206},{},[1207],{"type":32,"value":1208},"需要附加日志、缓存、权限",{"type":27,"tag":752,"props":1210,"children":1211},{},[1212],{"type":32,"value":1213},"代理",{"type":27,"tag":752,"props":1215,"children":1216},{},[1217],{"type":32,"value":1218},"包装后丢失原函数签名",{"type":27,"tag":49,"props":1220,"children":1221},{"id":344},[1222],{"type":32,"value":344},{"type":27,"tag":28,"props":1224,"children":1225},{},[1226],{"type":32,"value":1227},"TypeScript 里的设计模式，关键不是“更像面向对象”，而是让变化面、稳定面和边界责任表达得更清楚。工厂控制创建，策略控制规则替换，适配器隔离外部差异，代理保留签名的同时叠加附加能力。只要模式引入后类型信息还在，团队就能真正享受到抽象带来的收益。",{"type":27,"tag":28,"props":1229,"children":1230},{},[1231],{"type":32,"value":356},{"type":27,"tag":154,"props":1233,"children":1234},{},[1235,1244,1255],{"type":27,"tag":158,"props":1236,"children":1237},{},[1238,1240],{"type":32,"value":1239},"如果你想先稳住抽象层对外承诺，接着看 ",{"type":27,"tag":366,"props":1241,"children":1242},{"href":390},[1243],{"type":32,"value":393},{"type":27,"tag":158,"props":1245,"children":1246},{},[1247,1249],{"type":32,"value":1248},"若你要把模式继续落到测试和用例上，再看 ",{"type":27,"tag":366,"props":1250,"children":1252},{"href":1251},"/topics/typescript/typescript-test-data-builders-fixtures-mock-type-safety",[1253],{"type":32,"value":1254},"TypeScript 测试数据构建",{"type":27,"tag":158,"props":1256,"children":1257},{},[1258,1260],{"type":32,"value":1259},"如果你接下来要处理业务状态与错误流，再读 ",{"type":27,"tag":366,"props":1261,"children":1263},{"href":1262},"/topics/typescript/typescript-form-state-validation-error-modeling-guide",[1264],{"type":32,"value":1265},"TypeScript 表单与错误状态建模",{"type":27,"tag":28,"props":1267,"children":1268},{},[1269],{"type":32,"value":398},{"type":27,"tag":154,"props":1271,"children":1272},{},[1273,1282,1291,1298],{"type":27,"tag":158,"props":1274,"children":1275},{},[1276],{"type":27,"tag":366,"props":1277,"children":1279},{"href":1278},"/topics/typescript/typescript-generic-constraints-conditional-decision-guide",[1280],{"type":32,"value":1281},"TypeScript 泛型约束与条件泛型的实际决策",{"type":27,"tag":158,"props":1283,"children":1284},{},[1285],{"type":27,"tag":366,"props":1286,"children":1288},{"href":1287},"/topics/typescript/typescript-discriminated-unions-exhaustive-check-state-management",[1289],{"type":32,"value":1290},"TypeScript 可辨识联合与穷举检查",{"type":27,"tag":158,"props":1292,"children":1293},{},[1294],{"type":27,"tag":366,"props":1295,"children":1296},{"href":390},[1297],{"type":32,"value":393},{"type":27,"tag":158,"props":1299,"children":1300},{},[1301],{"type":27,"tag":366,"props":1302,"children":1304},{"href":1303},"/topics/frontend/vue-component-design-patterns-essentials",[1305],{"type":32,"value":1306},"Vue 组件设计模式精选",{"title":7,"searchDepth":437,"depth":437,"links":1308},[1309,1310,1311,1312,1313,1314,1315],{"id":933,"depth":440,"text":936},{"id":966,"depth":440,"text":969},{"id":999,"depth":440,"text":1002},{"id":1040,"depth":440,"text":1043},{"id":1065,"depth":440,"text":1068},{"id":1116,"depth":440,"text":1119},{"id":344,"depth":440,"text":344},"content:topics:typescript:typescript-design-patterns-factory-strategy-adapter-proxy.md","topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy.md","topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy",{"_path":1320,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1321,"description":1322,"date":10,"topic":5,"author":11,"tags":1323,"image":1328,"imageQuery":1329,"pexelsPhotoId":1330,"pexelsUrl":1331,"featured":6,"readingTime":1332,"body":1333,"_type":447,"_id":1848,"_source":449,"_file":1849,"_stem":1850,"_extension":452},"/topics/typescript/typescript-event-map-payload-contract-subscription-safety","TypeScript 事件系统建模：事件映射、payload 约束与订阅端类型安全","事件系统最容易失控的地方，不是发不出去，而是名字、payload 和订阅约定慢慢漂移。本文从 event map、发布订阅 API 设计和版本演进出发，讲清 TypeScript 如何让事件系统保持可协作。",[13,1324,1325,1326,1327],"Event Map","PubSub","Payload","Event Driven","/images/articles/typescript-event-map-payload-contract-subscription-safety-featured.jpg","event driven architecture code laptop",10826689,"https://www.pexels.com/photo/a-laptop-on-the-table-10826689/",16,{"type":24,"children":1334,"toc":1838},[1335,1372,1377,1383,1392,1397,1410,1415,1421,1426,1435,1440,1449,1454,1460,1465,1478,1490,1495,1513,1519,1524,1529,1547,1552,1558,1579,1597,1602,1608,1613,1636,1641,1646,1674,1678,1683,1688,1765,1769,1799,1803],{"type":27,"tag":28,"props":1336,"children":1337},{},[1338,1340,1346,1348,1354,1356,1362,1364,1370],{"type":32,"value":1339},"很多系统一开始的事件总线都很轻：",{"type":27,"tag":35,"props":1341,"children":1343},{"className":1342},[],[1344],{"type":32,"value":1345},"emit(name, payload)",{"type":32,"value":1347},"，再配一个 ",{"type":27,"tag":35,"props":1349,"children":1351},{"className":1350},[],[1352],{"type":32,"value":1353},"on(name, handler)",{"type":32,"value":1355}," 就能跑起来。真正的问题不会立刻出现，而是随着事件增多、订阅方变多、场景变复杂，名字和 payload 会慢慢失去同步。某个地方仍然发 ",{"type":27,"tag":35,"props":1357,"children":1359},{"className":1358},[],[1360],{"type":32,"value":1361},"user.updated",{"type":32,"value":1363},"，另一个地方已经开始监听 ",{"type":27,"tag":35,"props":1365,"children":1367},{"className":1366},[],[1368],{"type":32,"value":1369},"user.profile.updated",{"type":32,"value":1371},"；某次迭代里 payload 多了一个字段，但下游还按旧形态读取；有的 handler 以为收到的是单对象，另一个地方却开始传数组。",{"type":27,"tag":28,"props":1373,"children":1374},{},[1375],{"type":32,"value":1376},"TypeScript 在事件系统里的价值，不是把事件总线写得更花，而是把“事件名和 payload 的对应关系”变成可检查的契约。只要这一层没有被显式建模，系统后面再怎么拆模块、加队列、做回放，事件协作都很容易继续靠记忆和文档维持。",{"type":27,"tag":49,"props":1378,"children":1380},{"id":1379},"最危险的接口长这样事件名是-stringpayload-是-any",[1381],{"type":32,"value":1382},"最危险的接口长这样：事件名是 string，payload 是 any",{"type":27,"tag":61,"props":1384,"children":1387},{"className":1385,"code":1386,"language":66,"meta":7},[64],"function emit(event: string, payload: any) {\n  // ...\n}\n",[1388],{"type":27,"tag":35,"props":1389,"children":1390},{"__ignoreMap":7},[1391],{"type":32,"value":1386},{"type":27,"tag":28,"props":1393,"children":1394},{},[1395],{"type":32,"value":1396},"这类写法的坏处不是“没有类型提示”这么简单，而是它让系统的两个核心约束完全脱离了关系：",{"type":27,"tag":154,"props":1398,"children":1399},{},[1400,1405],{"type":27,"tag":158,"props":1401,"children":1402},{},[1403],{"type":32,"value":1404},"哪些事件名是合法的。",{"type":27,"tag":158,"props":1406,"children":1407},{},[1408],{"type":32,"value":1409},"某个事件对应的 payload 应该长什么样。",{"type":27,"tag":28,"props":1411,"children":1412},{},[1413],{"type":32,"value":1414},"只要这两个约束还靠人脑维护，规模一大就一定会漂移。",{"type":27,"tag":49,"props":1416,"children":1418},{"id":1417},"event-map-是最稳的起点先把名字和-payload-绑定起来",[1419],{"type":32,"value":1420},"event map 是最稳的起点：先把名字和 payload 绑定起来",{"type":27,"tag":28,"props":1422,"children":1423},{},[1424],{"type":32,"value":1425},"一种非常实用的建模方式，是先定义事件映射表：",{"type":27,"tag":61,"props":1427,"children":1430},{"className":1428,"code":1429,"language":66,"meta":7},[64],"type AppEventMap = {\n  'user.created': { id: string; source: 'admin' | 'self-service' }\n  'user.deleted': { id: string; reason?: string }\n  'invoice.paid': { invoiceId: string; amount: number }\n}\n",[1431],{"type":27,"tag":35,"props":1432,"children":1433},{"__ignoreMap":7},[1434],{"type":32,"value":1429},{"type":27,"tag":28,"props":1436,"children":1437},{},[1438],{"type":32,"value":1439},"一旦这张表存在，发布和订阅接口都可以围绕它推导：",{"type":27,"tag":61,"props":1441,"children":1444},{"className":1442,"code":1443,"language":66,"meta":7},[64],"function emit\u003CK extends keyof AppEventMap>(\n  event: K,\n  payload: AppEventMap[K]\n) {}\n\nfunction on\u003CK extends keyof AppEventMap>(\n  event: K,\n  handler: (payload: AppEventMap[K]) => void\n) {}\n",[1445],{"type":27,"tag":35,"props":1446,"children":1447},{"__ignoreMap":7},[1448],{"type":32,"value":1443},{"type":27,"tag":28,"props":1450,"children":1451},{},[1452],{"type":32,"value":1453},"这类 API 的意义不只是补全更舒服，而是让“事件名”和“事件负载”在类型层面无法脱钩。只要事件名选错、payload 形状不对，问题会在提交代码前就暴露。",{"type":27,"tag":49,"props":1455,"children":1457},{"id":1456},"事件设计真正要防的是同名异义和异名同义",[1458],{"type":32,"value":1459},"事件设计真正要防的是“同名异义”和“异名同义”",{"type":27,"tag":28,"props":1461,"children":1462},{},[1463],{"type":32,"value":1464},"事件系统最容易出现的两类混乱是：",{"type":27,"tag":154,"props":1466,"children":1467},{},[1468,1473],{"type":27,"tag":158,"props":1469,"children":1470},{},[1471],{"type":32,"value":1472},"同名异义：同一个事件名在不同模块里承载不同语义。",{"type":27,"tag":158,"props":1474,"children":1475},{},[1476],{"type":32,"value":1477},"异名同义：同一类业务事实被多个名字重复表达。",{"type":27,"tag":28,"props":1479,"children":1480},{},[1481,1483,1488],{"type":32,"value":1482},"比如 ",{"type":27,"tag":35,"props":1484,"children":1486},{"className":1485},[],[1487],{"type":32,"value":1361},{"type":32,"value":1489}," 这个名字，看起来什么都能装。用户改昵称、改角色、改手机号、改订阅偏好都能叫 updated。短期很方便，长期却让订阅方不知道自己到底该监听什么，也让 payload 越长越杂。",{"type":27,"tag":28,"props":1491,"children":1492},{},[1493],{"type":32,"value":1494},"更稳的做法往往是：",{"type":27,"tag":154,"props":1496,"children":1497},{},[1498,1503,1508],{"type":27,"tag":158,"props":1499,"children":1500},{},[1501],{"type":32,"value":1502},"名称按业务事实切分，而不是按“发生过变化”这种宽泛概念命名。",{"type":27,"tag":158,"props":1504,"children":1505},{},[1506],{"type":32,"value":1507},"payload 只携带当前事件需要承诺的最小信息。",{"type":27,"tag":158,"props":1509,"children":1510},{},[1511],{"type":32,"value":1512},"大量共享字段通过公共对象或 metadata 包装，而不是每个事件都随意展开。",{"type":27,"tag":49,"props":1514,"children":1516},{"id":1515},"订阅端类型安全的关键不在泛型而在-handler-语义是否稳定",[1517],{"type":32,"value":1518},"订阅端类型安全的关键，不在泛型，而在 handler 语义是否稳定",{"type":27,"tag":28,"props":1520,"children":1521},{},[1522],{"type":32,"value":1523},"发布端和订阅端的类型常常被讨论成“泛型能不能写出来”，其实更重要的问题是：handler 是否能基于事件名做出稳定假设。如果一个事件 payload 经常改、字段意义经常变，哪怕泛型写对了，订阅端也不会真正稳定。",{"type":27,"tag":28,"props":1525,"children":1526},{},[1527],{"type":32,"value":1528},"所以团队在设计 event map 时，最好把这几件事一起定清：",{"type":27,"tag":154,"props":1530,"children":1531},{},[1532,1537,1542],{"type":27,"tag":158,"props":1533,"children":1534},{},[1535],{"type":32,"value":1536},"这个事件代表什么业务事实。",{"type":27,"tag":158,"props":1538,"children":1539},{},[1540],{"type":32,"value":1541},"订阅方最少需要拿到哪些字段。",{"type":27,"tag":158,"props":1543,"children":1544},{},[1545],{"type":32,"value":1546},"哪些字段未来允许扩展，哪些字段一旦变化就算破坏式变更。",{"type":27,"tag":28,"props":1548,"children":1549},{},[1550],{"type":32,"value":1551},"TypeScript 能帮你守住结构，但不能替你定义清业务语义。",{"type":27,"tag":49,"props":1553,"children":1555},{"id":1554},"一个常见失败案例事件总线很通用但所有变更都在悄悄破约",[1556],{"type":32,"value":1557},"一个常见失败案例：事件总线很“通用”，但所有变更都在悄悄破约",{"type":27,"tag":28,"props":1559,"children":1560},{},[1561,1563,1569,1571,1577],{"type":32,"value":1562},"某团队有一套抽象得很漂亮的事件总线封装，",{"type":27,"tag":35,"props":1564,"children":1566},{"className":1565},[],[1567],{"type":32,"value":1568},"emit",{"type":32,"value":1570}," 和 ",{"type":27,"tag":35,"props":1572,"children":1574},{"className":1573},[],[1575],{"type":32,"value":1576},"on",{"type":32,"value":1578}," 都做成了泛型，但事件名本身没有中心化建模，而是由各模块自己声明字符串字面量。结果几个月后出现了典型问题：",{"type":27,"tag":154,"props":1580,"children":1581},{},[1582,1587,1592],{"type":27,"tag":158,"props":1583,"children":1584},{},[1585],{"type":32,"value":1586},"同一个名字在多个地方被重复定义。",{"type":27,"tag":158,"props":1588,"children":1589},{},[1590],{"type":32,"value":1591},"payload 字段逐步追加，但没有人通知所有订阅者。",{"type":27,"tag":158,"props":1593,"children":1594},{},[1595],{"type":32,"value":1596},"少数 handler 开始自己断言 payload 类型，绕过总线约束。",{"type":27,"tag":28,"props":1598,"children":1599},{},[1600],{"type":32,"value":1601},"问题不在总线 API，而在契约没有集中。只要 event map 不是系统级事实，而是多个文件分散声明，漂移迟早会发生。",{"type":27,"tag":49,"props":1603,"children":1605},{"id":1604},"事件版本演进要像-api-一样认真",[1606],{"type":32,"value":1607},"事件版本演进要像 API 一样认真",{"type":27,"tag":28,"props":1609,"children":1610},{},[1611],{"type":32,"value":1612},"很多团队对 HTTP API 很谨慎，却对事件变更很随意。实际上，事件一旦被多个消费者订阅，它就是另一种 API。比较值得固定的规则包括：",{"type":27,"tag":154,"props":1614,"children":1615},{},[1616,1621,1626,1631],{"type":27,"tag":158,"props":1617,"children":1618},{},[1619],{"type":32,"value":1620},"payload 新增字段通常问题不大，但删除和重命名要视作破坏式变更。",{"type":27,"tag":158,"props":1622,"children":1623},{},[1624],{"type":32,"value":1625},"需要重大语义变化时，优先引入新事件名，而不是偷偷改旧 payload。",{"type":27,"tag":158,"props":1627,"children":1628},{},[1629],{"type":32,"value":1630},"公共事件和内部事件分层，不要让局部实现细节暴露给全局订阅方。",{"type":27,"tag":158,"props":1632,"children":1633},{},[1634],{"type":32,"value":1635},"如果事件跨进程或跨服务传播，运行时 schema 校验也要补上。",{"type":27,"tag":28,"props":1637,"children":1638},{},[1639],{"type":32,"value":1640},"事件不是“消息发出去就算完”，而是长期协作契约的一部分。",{"type":27,"tag":49,"props":1642,"children":1644},{"id":1643},"一份事件系统建模检查表",[1645],{"type":32,"value":1643},{"type":27,"tag":154,"props":1647,"children":1648},{},[1649,1654,1659,1664,1669],{"type":27,"tag":158,"props":1650,"children":1651},{},[1652],{"type":32,"value":1653},"事件名和 payload 是否被一张中心化 event map 绑定。",{"type":27,"tag":158,"props":1655,"children":1656},{},[1657],{"type":32,"value":1658},"事件命名是否表达业务事实，而不是宽泛动作。",{"type":27,"tag":158,"props":1660,"children":1661},{},[1662],{"type":32,"value":1663},"payload 是否只承诺最小必要字段，而非把整个对象一股脑透出去。",{"type":27,"tag":158,"props":1665,"children":1666},{},[1667],{"type":32,"value":1668},"订阅方是否能仅凭事件名获得稳定的 payload 类型。",{"type":27,"tag":158,"props":1670,"children":1671},{},[1672],{"type":32,"value":1673},"破坏式变更是否按 API 升级一样被认真对待。",{"type":27,"tag":49,"props":1675,"children":1676},{"id":344},[1677],{"type":32,"value":344},{"type":27,"tag":28,"props":1679,"children":1680},{},[1681],{"type":32,"value":1682},"TypeScript 让事件系统最值得做的一件事，就是把“事件名和 payload 的关系”从口头约定变成编译期契约。只要 event map 足够集中、命名足够克制、版本演进足够明确，发布订阅就不会随着规模扩大而变成字符串驱动的隐性耦合。",{"type":27,"tag":28,"props":1684,"children":1685},{},[1686],{"type":32,"value":1687},"本批次专题导航：",{"type":27,"tag":154,"props":1689,"children":1690},{},[1691,1712,1735,1750],{"type":27,"tag":158,"props":1692,"children":1693},{},[1694,1696,1700,1702,1707,1708],{"type":32,"value":1695},"工程边界：",{"type":27,"tag":366,"props":1697,"children":1698},{"href":368},[1699],{"type":32,"value":371},{"type":32,"value":1701},"、",{"type":27,"tag":366,"props":1703,"children":1704},{"href":4},[1705],{"type":32,"value":1706},"TypeScript Monorepo 依赖边界治理",{"type":32,"value":1701},{"type":27,"tag":366,"props":1709,"children":1710},{"href":379},[1711],{"type":32,"value":382},{"type":27,"tag":158,"props":1713,"children":1714},{},[1715,1717,1721,1722,1728,1729],{"type":32,"value":1716},"协议协作：",{"type":27,"tag":366,"props":1718,"children":1719},{"href":390},[1720],{"type":32,"value":393},{"type":32,"value":1701},{"type":27,"tag":366,"props":1723,"children":1725},{"href":1724},"/topics/typescript/typescript-runtime-validation-static-types-schema-error-modeling",[1726],{"type":32,"value":1727},"TypeScript 运行时校验与静态类型协作",{"type":32,"value":1701},{"type":27,"tag":366,"props":1730,"children":1732},{"href":1731},"/topics/typescript/typescript-openapi-contract-codegen-handwritten-types-versioning",[1733],{"type":32,"value":1734},"TypeScript 与 OpenAPI 契约协同",{"type":27,"tag":158,"props":1736,"children":1737},{},[1738,1740,1745,1746],{"type":32,"value":1739},"落地复用：",{"type":27,"tag":366,"props":1741,"children":1742},{"href":899},[1743],{"type":32,"value":1744},"TypeScript 设计模式实战",{"type":32,"value":1701},{"type":27,"tag":366,"props":1747,"children":1748},{"href":1251},[1749],{"type":32,"value":1254},{"type":27,"tag":158,"props":1751,"children":1752},{},[1753,1755,1760,1761],{"type":32,"value":1754},"状态建模：",{"type":27,"tag":366,"props":1756,"children":1757},{"href":1320},[1758],{"type":32,"value":1759},"TypeScript 事件系统建模",{"type":32,"value":1701},{"type":27,"tag":366,"props":1762,"children":1763},{"href":1262},[1764],{"type":32,"value":1265},{"type":27,"tag":28,"props":1766,"children":1767},{},[1768],{"type":32,"value":356},{"type":27,"tag":154,"props":1770,"children":1771},{},[1772,1781,1790],{"type":27,"tag":158,"props":1773,"children":1774},{},[1775,1777],{"type":32,"value":1776},"如果你还没把边界输入做成可信数据，先读 ",{"type":27,"tag":366,"props":1778,"children":1779},{"href":1724},[1780],{"type":32,"value":1727},{"type":27,"tag":158,"props":1782,"children":1783},{},[1784,1786],{"type":32,"value":1785},"若你要把事件流继续落到页面状态，再看 ",{"type":27,"tag":366,"props":1787,"children":1788},{"href":1262},[1789],{"type":32,"value":1265},{"type":27,"tag":158,"props":1791,"children":1792},{},[1793,1795],{"type":32,"value":1794},"如果你希望对外契约和事件契约一起收口，可读 ",{"type":27,"tag":366,"props":1796,"children":1797},{"href":390},[1798],{"type":32,"value":393},{"type":27,"tag":28,"props":1800,"children":1801},{},[1802],{"type":32,"value":398},{"type":27,"tag":154,"props":1804,"children":1805},{},[1806,1815,1822,1829],{"type":27,"tag":158,"props":1807,"children":1808},{},[1809],{"type":27,"tag":366,"props":1810,"children":1812},{"href":1811},"/topics/typescript/typescript-template-literal-types-real-world-applications",[1813],{"type":32,"value":1814},"TypeScript 模板字面量类型在真实项目中的应用",{"type":27,"tag":158,"props":1816,"children":1817},{},[1818],{"type":27,"tag":366,"props":1819,"children":1820},{"href":1287},[1821],{"type":32,"value":1290},{"type":27,"tag":158,"props":1823,"children":1824},{},[1825],{"type":27,"tag":366,"props":1826,"children":1827},{"href":1724},[1828],{"type":32,"value":1727},{"type":27,"tag":158,"props":1830,"children":1831},{},[1832],{"type":27,"tag":366,"props":1833,"children":1835},{"href":1834},"/topics/realtime-applications-guide",[1836],{"type":32,"value":1837},"实时应用开发完全指南",{"title":7,"searchDepth":437,"depth":437,"links":1839},[1840,1841,1842,1843,1844,1845,1846,1847],{"id":1379,"depth":440,"text":1382},{"id":1417,"depth":440,"text":1420},{"id":1456,"depth":440,"text":1459},{"id":1515,"depth":440,"text":1518},{"id":1554,"depth":440,"text":1557},{"id":1604,"depth":440,"text":1607},{"id":1643,"depth":440,"text":1643},{"id":344,"depth":440,"text":344},"content:topics:typescript:typescript-event-map-payload-contract-subscription-safety.md","topics/typescript/typescript-event-map-payload-contract-subscription-safety.md","topics/typescript/typescript-event-map-payload-contract-subscription-safety",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":1852,"image":18,"imageQuery":19,"pexelsPhotoId":20,"pexelsUrl":21,"featured":6,"readingTime":22,"body":1853,"_type":447,"_id":448,"_source":449,"_file":450,"_stem":451,"_extension":452},[13,14,15,16,17],{"type":24,"children":1854,"toc":2173},[1855,1865,1869,1873,1877,1885,1889,1897,1901,1905,1921,1929,1939,1943,1947,1962,1966,1981,1985,1989,1993,2008,2012,2016,2032,2036,2058,2062,2066,2095,2099,2103,2107,2111,2138,2142],{"type":27,"tag":28,"props":1856,"children":1857},{},[1858,1859,1864],{"type":32,"value":33},{"type":27,"tag":35,"props":1860,"children":1862},{"className":1861},[],[1863],{"type":32,"value":40},{"type":32,"value":42},{"type":27,"tag":28,"props":1866,"children":1867},{},[1868],{"type":32,"value":47},{"type":27,"tag":49,"props":1870,"children":1871},{"id":51},[1872],{"type":32,"value":54},{"type":27,"tag":28,"props":1874,"children":1875},{},[1876],{"type":32,"value":59},{"type":27,"tag":61,"props":1878,"children":1880},{"className":1879,"code":65,"language":66,"meta":7},[64],[1881],{"type":27,"tag":35,"props":1882,"children":1883},{"__ignoreMap":7},[1884],{"type":32,"value":65},{"type":27,"tag":28,"props":1886,"children":1887},{},[1888],{"type":32,"value":76},{"type":27,"tag":61,"props":1890,"children":1892},{"className":1891,"code":80,"language":66,"meta":7},[64],[1893],{"type":27,"tag":35,"props":1894,"children":1895},{"__ignoreMap":7},[1896],{"type":32,"value":80},{"type":27,"tag":28,"props":1898,"children":1899},{},[1900],{"type":32,"value":90},{"type":27,"tag":49,"props":1902,"children":1903},{"id":93},[1904],{"type":32,"value":96},{"type":27,"tag":28,"props":1906,"children":1907},{},[1908,1909,1914,1915,1920],{"type":32,"value":101},{"type":27,"tag":35,"props":1910,"children":1912},{"className":1911},[],[1913],{"type":32,"value":107},{"type":32,"value":109},{"type":27,"tag":35,"props":1916,"children":1918},{"className":1917},[],[1919],{"type":32,"value":115},{"type":32,"value":117},{"type":27,"tag":61,"props":1922,"children":1924},{"className":1923,"code":122,"language":123,"meta":7},[121],[1925],{"type":27,"tag":35,"props":1926,"children":1927},{"__ignoreMap":7},[1928],{"type":32,"value":122},{"type":27,"tag":28,"props":1930,"children":1931},{},[1932,1933,1938],{"type":32,"value":133},{"type":27,"tag":35,"props":1934,"children":1936},{"className":1935},[],[1937],{"type":32,"value":139},{"type":32,"value":141},{"type":27,"tag":49,"props":1940,"children":1941},{"id":144},[1942],{"type":32,"value":147},{"type":27,"tag":28,"props":1944,"children":1945},{},[1946],{"type":32,"value":152},{"type":27,"tag":154,"props":1948,"children":1949},{},[1950,1954,1958],{"type":27,"tag":158,"props":1951,"children":1952},{},[1953],{"type":32,"value":162},{"type":27,"tag":158,"props":1955,"children":1956},{},[1957],{"type":32,"value":167},{"type":27,"tag":158,"props":1959,"children":1960},{},[1961],{"type":32,"value":172},{"type":27,"tag":28,"props":1963,"children":1964},{},[1965],{"type":32,"value":177},{"type":27,"tag":154,"props":1967,"children":1968},{},[1969,1973,1977],{"type":27,"tag":158,"props":1970,"children":1971},{},[1972],{"type":32,"value":185},{"type":27,"tag":158,"props":1974,"children":1975},{},[1976],{"type":32,"value":190},{"type":27,"tag":158,"props":1978,"children":1979},{},[1980],{"type":32,"value":195},{"type":27,"tag":28,"props":1982,"children":1983},{},[1984],{"type":32,"value":200},{"type":27,"tag":49,"props":1986,"children":1987},{"id":203},[1988],{"type":32,"value":206},{"type":27,"tag":28,"props":1990,"children":1991},{},[1992],{"type":32,"value":211},{"type":27,"tag":154,"props":1994,"children":1995},{},[1996,2000,2004],{"type":27,"tag":158,"props":1997,"children":1998},{},[1999],{"type":32,"value":219},{"type":27,"tag":158,"props":2001,"children":2002},{},[2003],{"type":32,"value":224},{"type":27,"tag":158,"props":2005,"children":2006},{},[2007],{"type":32,"value":229},{"type":27,"tag":28,"props":2009,"children":2010},{},[2011],{"type":32,"value":234},{"type":27,"tag":49,"props":2013,"children":2014},{"id":237},[2015],{"type":32,"value":240},{"type":27,"tag":28,"props":2017,"children":2018},{},[2019,2020,2025,2026,2031],{"type":32,"value":245},{"type":27,"tag":35,"props":2021,"children":2023},{"className":2022},[],[2024],{"type":32,"value":251},{"type":32,"value":253},{"type":27,"tag":35,"props":2027,"children":2029},{"className":2028},[],[2030],{"type":32,"value":251},{"type":32,"value":260},{"type":27,"tag":28,"props":2033,"children":2034},{},[2035],{"type":32,"value":265},{"type":27,"tag":154,"props":2037,"children":2038},{},[2039,2049],{"type":27,"tag":158,"props":2040,"children":2041},{},[2042,2043,2048],{"type":32,"value":273},{"type":27,"tag":35,"props":2044,"children":2046},{"className":2045},[],[2047],{"type":32,"value":251},{"type":32,"value":280},{"type":27,"tag":158,"props":2050,"children":2051},{},[2052,2057],{"type":27,"tag":35,"props":2053,"children":2055},{"className":2054},[],[2056],{"type":32,"value":251},{"type":32,"value":290},{"type":27,"tag":28,"props":2059,"children":2060},{},[2061],{"type":32,"value":295},{"type":27,"tag":49,"props":2063,"children":2064},{"id":298},[2065],{"type":32,"value":298},{"type":27,"tag":302,"props":2067,"children":2068},{},[2069,2073,2083,2087,2091],{"type":27,"tag":158,"props":2070,"children":2071},{},[2072],{"type":32,"value":309},{"type":27,"tag":158,"props":2074,"children":2075},{},[2076,2077,2082],{"type":32,"value":314},{"type":27,"tag":35,"props":2078,"children":2080},{"className":2079},[],[2081],{"type":32,"value":115},{"type":32,"value":321},{"type":27,"tag":158,"props":2084,"children":2085},{},[2086],{"type":32,"value":326},{"type":27,"tag":158,"props":2088,"children":2089},{},[2090],{"type":32,"value":331},{"type":27,"tag":158,"props":2092,"children":2093},{},[2094],{"type":32,"value":336},{"type":27,"tag":28,"props":2096,"children":2097},{},[2098],{"type":32,"value":341},{"type":27,"tag":49,"props":2100,"children":2101},{"id":344},[2102],{"type":32,"value":344},{"type":27,"tag":28,"props":2104,"children":2105},{},[2106],{"type":32,"value":351},{"type":27,"tag":28,"props":2108,"children":2109},{},[2110],{"type":32,"value":356},{"type":27,"tag":154,"props":2112,"children":2113},{},[2114,2122,2130],{"type":27,"tag":158,"props":2115,"children":2116},{},[2117,2118],{"type":32,"value":364},{"type":27,"tag":366,"props":2119,"children":2120},{"href":368},[2121],{"type":32,"value":371},{"type":27,"tag":158,"props":2123,"children":2124},{},[2125,2126],{"type":32,"value":376},{"type":27,"tag":366,"props":2127,"children":2128},{"href":379},[2129],{"type":32,"value":382},{"type":27,"tag":158,"props":2131,"children":2132},{},[2133,2134],{"type":32,"value":387},{"type":27,"tag":366,"props":2135,"children":2136},{"href":390},[2137],{"type":32,"value":393},{"type":27,"tag":28,"props":2139,"children":2140},{},[2141],{"type":32,"value":398},{"type":27,"tag":154,"props":2143,"children":2144},{},[2145,2152,2159,2166],{"type":27,"tag":158,"props":2146,"children":2147},{},[2148],{"type":27,"tag":366,"props":2149,"children":2150},{"href":368},[2151],{"type":32,"value":371},{"type":27,"tag":158,"props":2153,"children":2154},{},[2155],{"type":27,"tag":366,"props":2156,"children":2157},{"href":414},[2158],{"type":32,"value":417},{"type":27,"tag":158,"props":2160,"children":2161},{},[2162],{"type":27,"tag":366,"props":2163,"children":2164},{"href":423},[2165],{"type":32,"value":426},{"type":27,"tag":158,"props":2167,"children":2168},{},[2169],{"type":27,"tag":366,"props":2170,"children":2171},{"href":432},[2172],{"type":32,"value":435},{"title":7,"searchDepth":437,"depth":437,"links":2174},[2175,2176,2177,2178,2179,2180,2181],{"id":51,"depth":440,"text":54},{"id":93,"depth":440,"text":96},{"id":144,"depth":440,"text":147},{"id":203,"depth":440,"text":206},{"id":237,"depth":440,"text":240},{"id":298,"depth":440,"text":298},{"id":344,"depth":440,"text":344},1781081291352]