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