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