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