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