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