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