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