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