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