[{"data":1,"prerenderedAt":2992},["ShallowReactive",2],{"article-/topics/typescript/typescript-mapped-types-key-remapping-advanced-patterns":3,"related-typescript":759,"content-query-Ki3hAbd1DO":2402},{"_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,1885],{"_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":10,"topic":5,"author":11,"tags":1202,"image":1208,"imageQuery":1209,"pexelsPhotoId":1210,"pexelsUrl":1211,"featured":6,"readingTime":1212,"body":1213,"_type":753,"_id":1882,"_source":755,"_file":1883,"_stem":1884,"_extension":758},"/topics/typescript/typescript-conditional-types-infer-pattern-matching","TypeScript 条件类型与 infer：从模式匹配到递归类型推导的实战路径","条件类型和 infer 是 TypeScript 类型系统中最灵活也最容易被误用的机制。本文从实际代码场景出发，讲清条件类型的匹配规则、infer 的推断时机、递归类型推导的边界，以及它们如何帮助写出更精确的工具类型。",[13,1203,1204,1205,1206,1207],"条件类型","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":24,"children":1214,"toc":1869},[1215,1251,1256,1262,1290,1299,1304,1313,1334,1346,1355,1360,1369,1375,1385,1390,1399,1404,1480,1492,1501,1518,1524,1529,1538,1543,1552,1558,1567,1573,1582,1595,1601,1610,1616,1621,1630,1635,1663,1669,1674,1683,1688,1697,1702,1707,1712,1721,1726,1732,1864],{"type":27,"tag":28,"props":1216,"children":1217},{},[1218,1220,1225,1227,1233,1235,1241,1243,1249],{"type":32,"value":1219},"条件类型和 ",{"type":27,"tag":35,"props":1221,"children":1223},{"className":1222},[],[1224],{"type":32,"value":1204},{"type":32,"value":1226}," 是 TypeScript 类型系统里最强大的两个机制，也是很多人文档看完但不会用的两个概念。文档告诉你 ",{"type":27,"tag":35,"props":1228,"children":1230},{"className":1229},[],[1231],{"type":32,"value":1232},"T extends U ? X : Y",{"type":32,"value":1234}," 是条件类型，",{"type":27,"tag":35,"props":1236,"children":1238},{"className":1237},[],[1239],{"type":32,"value":1240},"infer R",{"type":32,"value":1242}," 可以提取类型变量，但实际看到 ",{"type":27,"tag":35,"props":1244,"children":1246},{"className":1245},[],[1247],{"type":32,"value":1248},"ReturnType\u003CT>",{"type":32,"value":1250}," 的源码时还是懵的。",{"type":27,"tag":28,"props":1252,"children":1253},{},[1254],{"type":32,"value":1255},"这不是理解能力的问题。条件类型的难点不在语法，而在它运行在一个和值空间完全不同的逻辑系统里。类型不是数据，不能计算，不存在运行时流程控制。条件类型的\"条件\"本质上是类型结构的模式匹配——它不看取值范围，只看结构形状。",{"type":27,"tag":59,"props":1257,"children":1259},{"id":1258},"条件类型的匹配规则结构兼容不是完全相等",[1260],{"type":32,"value":1261},"条件类型的匹配规则：结构兼容，不是完全相等",{"type":27,"tag":28,"props":1263,"children":1264},{},[1265,1267,1273,1275,1281,1283,1288],{"type":32,"value":1266},"条件类型 ",{"type":27,"tag":35,"props":1268,"children":1270},{"className":1269},[],[1271],{"type":32,"value":1272},"T extends U ? A : B",{"type":32,"value":1274}," 的判断标准，和函数签名里的 ",{"type":27,"tag":35,"props":1276,"children":1278},{"className":1277},[],[1279],{"type":32,"value":1280},"extends",{"type":32,"value":1282}," 约束其实是同一套规则：",{"type":27,"tag":1054,"props":1284,"children":1285},{},[1286],{"type":32,"value":1287},"U 能不能收容 T",{"type":32,"value":1289},"。不是 T 和 U 长得一样，而是 T 的类型的值能不能赋值给 U。",{"type":27,"tag":70,"props":1291,"children":1294},{"className":1292,"code":1293,"language":5,"meta":7},[73],"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",[1295],{"type":27,"tag":35,"props":1296,"children":1297},{"__ignoreMap":7},[1298],{"type":32,"value":1293},{"type":27,"tag":28,"props":1300,"children":1301},{},[1302],{"type":32,"value":1303},"看起来简单，但换成对象类型就很容易判断错：",{"type":27,"tag":70,"props":1305,"children":1308},{"className":1306,"code":1307,"language":5,"meta":7},[73],"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",[1309],{"type":27,"tag":35,"props":1310,"children":1311},{"__ignoreMap":7},[1312],{"type":32,"value":1307},{"type":27,"tag":28,"props":1314,"children":1315},{},[1316,1318,1324,1326,1332],{"type":32,"value":1317},"这里经常有人困惑：为什么 ",{"type":27,"tag":35,"props":1319,"children":1321},{"className":1320},[],[1322],{"type":32,"value":1323},"{ name: string; age: number }",{"type":32,"value":1325}," 能匹配 ",{"type":27,"tag":35,"props":1327,"children":1329},{"className":1328},[],[1330],{"type":32,"value":1331},"{ name: string }",{"type":32,"value":1333},"？因为条件类型看的是 U（右侧）能不能收容 T（左侧），不是反过来。对象类型的兼容性是结构子类型——多字段的结构可以被少字段的结构收容，只要被检查的字段满足要求。",{"type":27,"tag":28,"props":1335,"children":1336},{},[1337,1339,1344],{"type":32,"value":1338},"把这个规则反过来，就是 ",{"type":27,"tag":35,"props":1340,"children":1342},{"className":1341},[],[1343],{"type":32,"value":1280},{"type":32,"value":1345}," 约束里的分配条件类型（distributive conditional types）：",{"type":27,"tag":70,"props":1347,"children":1350},{"className":1348,"code":1349,"language":5,"meta":7},[73],"type ToArray\u003CT> = T extends unknown ? T[] : never\n\ntype R7 = ToArray\u003Cstring | number>\n// string[] | number[] —— 不是 (string | number)[]\n",[1351],{"type":27,"tag":35,"props":1352,"children":1353},{"__ignoreMap":7},[1354],{"type":32,"value":1349},{"type":27,"tag":28,"props":1356,"children":1357},{},[1358],{"type":32,"value":1359},"联合类型会被拆开分别匹配再合并，这就是分配律。关掉分配律的方法是用方括号包裹泛型参数：",{"type":27,"tag":70,"props":1361,"children":1364},{"className":1362,"code":1363,"language":5,"meta":7},[73],"type ToArrayNonDist\u003CT> = [T] extends [unknown] ? T[] : never\n\ntype R8 = ToArrayNonDist\u003Cstring | number>\n// (string | number)[]\n",[1365],{"type":27,"tag":35,"props":1366,"children":1367},{"__ignoreMap":7},[1368],{"type":32,"value":1363},{"type":27,"tag":59,"props":1370,"children":1372},{"id":1371},"infer-的实质在匹配位置声明类型变量",[1373],{"type":32,"value":1374},"infer 的实质：在匹配位置声明类型变量",{"type":27,"tag":28,"props":1376,"children":1377},{},[1378,1383],{"type":27,"tag":35,"props":1379,"children":1381},{"className":1380},[],[1382],{"type":32,"value":1204},{"type":32,"value":1384}," 不是什么黑魔法——它只是在条件类型的 extends 子句中声明一个待推导的类型变量，然后让 TypeScript 在匹配过程中自动填充它。",{"type":27,"tag":28,"props":1386,"children":1387},{},[1388],{"type":32,"value":1389},"最简单的例子是从函数类型提取返回值类型：",{"type":27,"tag":70,"props":1391,"children":1394},{"className":1392,"code":1393,"language":5,"meta":7},[73],"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",[1395],{"type":27,"tag":35,"props":1396,"children":1397},{"__ignoreMap":7},[1398],{"type":32,"value":1393},{"type":27,"tag":28,"props":1400,"children":1401},{},[1402],{"type":32,"value":1403},"流程是这样的：",{"type":27,"tag":204,"props":1405,"children":1406},{},[1407,1418,1429,1454,1470],{"type":27,"tag":208,"props":1408,"children":1409},{},[1410,1412],{"type":32,"value":1411},"传入 ",{"type":27,"tag":35,"props":1413,"children":1415},{"className":1414},[],[1416],{"type":32,"value":1417},"(x: number, y: string) => boolean",{"type":27,"tag":208,"props":1419,"children":1420},{},[1421,1423],{"type":32,"value":1422},"检查 ",{"type":27,"tag":35,"props":1424,"children":1426},{"className":1425},[],[1427],{"type":32,"value":1428},"(x: number, y: string) => boolean extends (...args: any[]) => infer R",{"type":27,"tag":208,"props":1430,"children":1431},{},[1432,1438,1440,1446,1448],{"type":27,"tag":35,"props":1433,"children":1435},{"className":1434},[],[1436],{"type":32,"value":1437},"(...args: any[])",{"type":32,"value":1439}," 匹配 ",{"type":27,"tag":35,"props":1441,"children":1443},{"className":1442},[],[1444],{"type":32,"value":1445},"(x: number, y: string)",{"type":32,"value":1447},"，剩余 ",{"type":27,"tag":35,"props":1449,"children":1451},{"className":1450},[],[1452],{"type":32,"value":1453},"=> boolean",{"type":27,"tag":208,"props":1455,"children":1456},{},[1457,1462,1464],{"type":27,"tag":35,"props":1458,"children":1460},{"className":1459},[],[1461],{"type":32,"value":1240},{"type":32,"value":1463}," 被推导为 ",{"type":27,"tag":35,"props":1465,"children":1467},{"className":1466},[],[1468],{"type":32,"value":1469},"boolean",{"type":27,"tag":208,"props":1471,"children":1472},{},[1473,1475],{"type":32,"value":1474},"返回 ",{"type":27,"tag":35,"props":1476,"children":1478},{"className":1477},[],[1479],{"type":32,"value":1469},{"type":27,"tag":28,"props":1481,"children":1482},{},[1483,1485,1490],{"type":32,"value":1484},"但 ",{"type":27,"tag":35,"props":1486,"children":1488},{"className":1487},[],[1489],{"type":32,"value":1204},{"type":32,"value":1491}," 有一个容易踩的坑——它只在条件类型为 true 的分支里可用：",{"type":27,"tag":70,"props":1493,"children":1496},{"className":1494,"code":1495,"language":5,"meta":7},[73],"// 错误用法\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",[1497],{"type":27,"tag":35,"props":1498,"children":1499},{"__ignoreMap":7},[1500],{"type":32,"value":1495},{"type":27,"tag":28,"props":1502,"children":1503},{},[1504,1509,1511,1516],{"type":27,"tag":35,"props":1505,"children":1507},{"className":1506},[],[1508],{"type":32,"value":1204},{"type":32,"value":1510}," 必须出现在 ",{"type":27,"tag":35,"props":1512,"children":1514},{"className":1513},[],[1515],{"type":32,"value":1280},{"type":32,"value":1517}," 子句的右侧，而且只能在条件为 true 时才能被使用。",{"type":27,"tag":59,"props":1519,"children":1521},{"id":1520},"递归类型推导条件类型-infer-的组合",[1522],{"type":32,"value":1523},"递归类型推导：条件类型 + infer 的组合",{"type":27,"tag":28,"props":1525,"children":1526},{},[1527],{"type":32,"value":1528},"真正体现 infer 威力的是递归类型推导。考虑一个场景：从嵌套对象中提取所有叶子路径的值的类型。",{"type":27,"tag":70,"props":1530,"children":1533},{"className":1531,"code":1532,"language":5,"meta":7},[73],"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",[1534],{"type":27,"tag":35,"props":1535,"children":1536},{"__ignoreMap":7},[1537],{"type":32,"value":1532},{"type":27,"tag":28,"props":1539,"children":1540},{},[1541],{"type":32,"value":1542},"递归类型推导需要特别注意终止条件。如果没有终止条件，TypeScript 会达到递归深度上限（通常是 50 层）然后报错：",{"type":27,"tag":70,"props":1544,"children":1547},{"className":1545,"code":1546,"language":5,"meta":7},[73],"type InfiniteDeep\u003CT> = T extends Record\u003Cstring, infer V>\n  ? InfiniteDeep\u003CV>\n  : never\n// 编译器可能报 \"Type instantiation is excessively deep and possibly infinite\"\n",[1548],{"type":27,"tag":35,"props":1549,"children":1550},{"__ignoreMap":7},[1551],{"type":32,"value":1546},{"type":27,"tag":264,"props":1553,"children":1555},{"id":1554},"实用模式-1展开-promise",[1556],{"type":32,"value":1557},"实用模式 1：展开 Promise",{"type":27,"tag":70,"props":1559,"children":1562},{"className":1560,"code":1561,"language":5,"meta":7},[73],"type Unwrap\u003CT> = T extends Promise\u003Cinfer U> ? Unwrap\u003CU> : T\n\ntype R11 = Unwrap\u003CPromise\u003CPromise\u003CPromise\u003Cstring>>>>\n// string —— 递归展开所有层级\n",[1563],{"type":27,"tag":35,"props":1564,"children":1565},{"__ignoreMap":7},[1566],{"type":32,"value":1561},{"type":27,"tag":264,"props":1568,"children":1570},{"id":1569},"实用模式-2提取函数参数类型",[1571],{"type":32,"value":1572},"实用模式 2：提取函数参数类型",{"type":27,"tag":70,"props":1574,"children":1577},{"className":1575,"code":1576,"language":5,"meta":7},[73],"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",[1578],{"type":27,"tag":35,"props":1579,"children":1580},{"__ignoreMap":7},[1581],{"type":32,"value":1576},{"type":27,"tag":28,"props":1583,"children":1584},{},[1585,1587,1593],{"type":32,"value":1586},"注意这里 infer P 不是单个类型，而是参数元组。TypeScript 会把 ",{"type":27,"tag":35,"props":1588,"children":1590},{"className":1589},[],[1591],{"type":32,"value":1592},"...args",{"type":32,"value":1594}," 的展开类型自动推断为元组。",{"type":27,"tag":264,"props":1596,"children":1598},{"id":1597},"实用模式-3条件推断-模板字面量",[1599],{"type":32,"value":1600},"实用模式 3：条件推断 + 模板字面量",{"type":27,"tag":70,"props":1602,"children":1605},{"className":1603,"code":1604,"language":5,"meta":7},[73],"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",[1606],{"type":27,"tag":35,"props":1607,"children":1608},{"__ignoreMap":7},[1609],{"type":32,"value":1604},{"type":27,"tag":59,"props":1611,"children":1613},{"id":1612},"实战场景实现一个类型安全的路径提取器",[1614],{"type":32,"value":1615},"实战场景：实现一个类型安全的路径提取器",{"type":27,"tag":28,"props":1617,"children":1618},{},[1619],{"type":32,"value":1620},"下面用一个稍微复杂一点的例子，展示条件类型和 infer 的组合威力。假设你需要一个类型，能从深层嵌套对象中提取指定路径的值类型：",{"type":27,"tag":70,"props":1622,"children":1625},{"className":1623,"code":1624,"language":5,"meta":7},[73],"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",[1626],{"type":27,"tag":35,"props":1627,"children":1628},{"__ignoreMap":7},[1629],{"type":32,"value":1624},{"type":27,"tag":28,"props":1631,"children":1632},{},[1633],{"type":32,"value":1634},"这个类型有两个递归分支：",{"type":27,"tag":204,"props":1636,"children":1637},{},[1638,1651],{"type":27,"tag":208,"props":1639,"children":1640},{},[1641,1643,1649],{"type":32,"value":1642},"如果路径包含 ",{"type":27,"tag":35,"props":1644,"children":1646},{"className":1645},[],[1647],{"type":32,"value":1648},".",{"type":32,"value":1650},"，用模板字面量拆分出第一段和剩余路径",{"type":27,"tag":208,"props":1652,"children":1653},{},[1654,1656,1661],{"type":32,"value":1655},"如果路径不包含 ",{"type":27,"tag":35,"props":1657,"children":1659},{"className":1658},[],[1660],{"type":32,"value":1648},{"type":32,"value":1662},"，直接取 key",{"type":27,"tag":59,"props":1664,"children":1666},{"id":1665},"infer-的逆协变与逆变",[1667],{"type":32,"value":1668},"infer 的逆协变与逆变",{"type":27,"tag":28,"props":1670,"children":1671},{},[1672],{"type":32,"value":1673},"当 infer 出现在不同位置时，它的赋值行为不同。这是很多人踩坑的地方：",{"type":27,"tag":70,"props":1675,"children":1678},{"className":1676,"code":1677,"language":5,"meta":7},[73],"// 协变位置（函数返回值）\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",[1679],{"type":27,"tag":35,"props":1680,"children":1681},{"__ignoreMap":7},[1682],{"type":32,"value":1677},{"type":27,"tag":28,"props":1684,"children":1685},{},[1686],{"type":32,"value":1687},"但在泛型中，逆变位置的 infer 会有不同的行为：",{"type":27,"tag":70,"props":1689,"children":1692},{"className":1690,"code":1691,"language":5,"meta":7},[73],"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",[1693],{"type":27,"tag":35,"props":1694,"children":1695},{"__ignoreMap":7},[1696],{"type":32,"value":1691},{"type":27,"tag":28,"props":1698,"children":1699},{},[1700],{"type":32,"value":1701},"这个技巧利用了函数参数的逆变位置：当多个类型分布在同一个逆变位置上时，TypeScript 会尝试取交集。",{"type":27,"tag":59,"props":1703,"children":1705},{"id":1704},"递归类型推导的性能问题",[1706],{"type":32,"value":1704},{"type":27,"tag":28,"props":1708,"children":1709},{},[1710],{"type":32,"value":1711},"递归条件类型不是免费的。每层递归都会增加类型实例化的深度。以下场景尤其需要注意：",{"type":27,"tag":70,"props":1713,"children":1716},{"className":1714,"code":1715,"language":5,"meta":7},[73],"// 逐个元素处理的递归（性能较差的写法）\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",[1717],{"type":27,"tag":35,"props":1718,"children":1719},{"__ignoreMap":7},[1720],{"type":32,"value":1715},{"type":27,"tag":28,"props":1722,"children":1723},{},[1724],{"type":32,"value":1725},"映射类型的单层操作不会增加递归深度，而递归条件类型每展开一层都计数。对于可能超过 30 层嵌套的结构，先评估是否真的需要这么深的递归。",{"type":27,"tag":59,"props":1727,"children":1729},{"id":1728},"总结条件类型-infer-的使用建议",[1730],{"type":32,"value":1731},"总结：条件类型 + infer 的使用建议",{"type":27,"tag":552,"props":1733,"children":1734},{},[1735,1755],{"type":27,"tag":556,"props":1736,"children":1737},{},[1738],{"type":27,"tag":560,"props":1739,"children":1740},{},[1741,1745,1750],{"type":27,"tag":564,"props":1742,"children":1743},{},[1744],{"type":32,"value":568},{"type":27,"tag":564,"props":1746,"children":1747},{},[1748],{"type":32,"value":1749},"使用方式",{"type":27,"tag":564,"props":1751,"children":1752},{},[1753],{"type":32,"value":1754},"注意事项",{"type":27,"tag":580,"props":1756,"children":1757},{},[1758,1780,1802,1828,1846],{"type":27,"tag":560,"props":1759,"children":1760},{},[1761,1766,1775],{"type":27,"tag":587,"props":1762,"children":1763},{},[1764],{"type":32,"value":1765},"提取函数返回值类型",{"type":27,"tag":587,"props":1767,"children":1768},{},[1769],{"type":27,"tag":35,"props":1770,"children":1772},{"className":1771},[],[1773],{"type":32,"value":1774},"(...args: any[]) => infer R",{"type":27,"tag":587,"props":1776,"children":1777},{},[1778],{"type":32,"value":1779},"简单直接，无限参数兼容",{"type":27,"tag":560,"props":1781,"children":1782},{},[1783,1788,1797],{"type":27,"tag":587,"props":1784,"children":1785},{},[1786],{"type":32,"value":1787},"提取 Promise 内部类型",{"type":27,"tag":587,"props":1789,"children":1790},{},[1791],{"type":27,"tag":35,"props":1792,"children":1794},{"className":1793},[],[1795],{"type":32,"value":1796},"Promise\u003Cinfer T>",{"type":27,"tag":587,"props":1798,"children":1799},{},[1800],{"type":32,"value":1801},"递归展开需加终止条件",{"type":27,"tag":560,"props":1803,"children":1804},{},[1805,1810,1815],{"type":27,"tag":587,"props":1806,"children":1807},{},[1808],{"type":32,"value":1809},"路径拆分",{"type":27,"tag":587,"props":1811,"children":1812},{},[1813],{"type":32,"value":1814},"模板字面量 + infer",{"type":27,"tag":587,"props":1816,"children":1817},{},[1818,1820,1826],{"type":32,"value":1819},"注意 ",{"type":27,"tag":35,"props":1821,"children":1823},{"className":1822},[],[1824],{"type":32,"value":1825},"keyof",{"type":32,"value":1827}," 配合",{"type":27,"tag":560,"props":1829,"children":1830},{},[1831,1836,1841],{"type":27,"tag":587,"props":1832,"children":1833},{},[1834],{"type":32,"value":1835},"联合转交叉",{"type":27,"tag":587,"props":1837,"children":1838},{},[1839],{"type":32,"value":1840},"逆变位置 infer",{"type":27,"tag":587,"props":1842,"children":1843},{},[1844],{"type":32,"value":1845},"理解原理再使用",{"type":27,"tag":560,"props":1847,"children":1848},{},[1849,1854,1859],{"type":27,"tag":587,"props":1850,"children":1851},{},[1852],{"type":32,"value":1853},"递归对象类型",{"type":27,"tag":587,"props":1855,"children":1856},{},[1857],{"type":32,"value":1858},"映射类型 + 条件",{"type":27,"tag":587,"props":1860,"children":1861},{},[1862],{"type":32,"value":1863},"评估深度，避免爆炸",{"type":27,"tag":28,"props":1865,"children":1866},{},[1867],{"type":32,"value":1868},"条件类型的难点从来不在语法，而在于它遵循的是结构子类型规则，不是等值规则。写条件类型时，心里应该想着\"这个形状能不能匹配那个形状\"，而不是\"这两个值相不相等\"。",{"title":7,"searchDepth":732,"depth":732,"links":1870},[1871,1872,1873,1878,1879,1880,1881],{"id":1258,"depth":735,"text":1261},{"id":1371,"depth":735,"text":1374},{"id":1520,"depth":735,"text":1523,"children":1874},[1875,1876,1877],{"id":1554,"depth":732,"text":1557},{"id":1569,"depth":732,"text":1572},{"id":1597,"depth":732,"text":1600},{"id":1612,"depth":735,"text":1615},{"id":1665,"depth":735,"text":1668},{"id":1704,"depth":735,"text":1704},{"id":1728,"depth":735,"text":1731},"content:topics:typescript:typescript-conditional-types-infer-pattern-matching.md","topics/typescript/typescript-conditional-types-infer-pattern-matching.md","topics/typescript/typescript-conditional-types-infer-pattern-matching",{"_path":1886,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1887,"description":1888,"date":10,"topic":5,"author":11,"tags":1889,"image":1895,"imageQuery":1896,"pexelsPhotoId":1897,"pexelsUrl":1898,"featured":6,"readingTime":1899,"body":1900,"_type":753,"_id":2399,"_source":755,"_file":2400,"_stem":2401,"_extension":758},"/topics/typescript/typescript-decorators-real-world-validation-logging-di","TypeScript 装饰器的实际使用场景","TypeScript 装饰器在经历了多次提案变更后，终于在 5.0 版本支持了 ECMAScript 标准的装饰器。本文对比新旧装饰器语法，讲解类装饰器、方法装饰器、访问器装饰器和属性装饰器的实际用途，并用四种模式展示它们在项目中的应用。",[13,1890,1891,1892,1893,1894],"装饰器","依赖注入","日志","校验","AOP","/images/articles/typescript-decorators-real-world-validation-logging-di-featured.jpg","typescript decorator pattern code programming laptop",34804022,"https://www.pexels.com/photo/colorful-code-on-screen-with-light-patterns-34804022/",16,{"type":24,"children":1901,"toc":2390},[1902,1923,1928,1933,1938,1947,1967,1973,1978,1987,1992,2000,2005,2011,2016,2025,2030,2036,2041,2050,2056,2061,2070,2083,2089,2094,2227,2232,2241,2282,2286,2385],{"type":27,"tag":28,"props":1903,"children":1904},{},[1905,1907,1913,1915,1921],{"type":32,"value":1906},"TypeScript 装饰器经历了一段不太平滑的演进。早期版本（5.0 之前）使用的是实验性的\"传统装饰器\"（experimental decorators），需要在 ",{"type":27,"tag":35,"props":1908,"children":1910},{"className":1909},[],[1911],{"type":32,"value":1912},"tsconfig.json",{"type":32,"value":1914}," 中开启 ",{"type":27,"tag":35,"props":1916,"children":1918},{"className":1917},[],[1919],{"type":32,"value":1920},"experimentalDecorators: true",{"type":32,"value":1922},"。5.0 之后开始支持 ECMAScript 标准的装饰器提案（Stage 3），默认不兼容。",{"type":27,"tag":28,"props":1924,"children":1925},{},[1926],{"type":32,"value":1927},"目前大部分遗留项目仍然在用传统装饰器，新项目可以逐步采用标准装饰器。本文以传统装饰器为例来说明模式——这些模式在新标准下同样适用，只是语法和参数有差异。",{"type":27,"tag":59,"props":1929,"children":1931},{"id":1930},"四种装饰器的签名",[1932],{"type":32,"value":1930},{"type":27,"tag":28,"props":1934,"children":1935},{},[1936],{"type":32,"value":1937},"传统装饰器有四类，每种接收的参数不同：",{"type":27,"tag":70,"props":1939,"children":1942},{"code":1940,"language":5,"meta":7,"className":1941},"// 类装饰器\ntype ClassDecorator = (target: Function) => void | Function\n\n// 方法装饰器\ntype MethodDecorator = \u003CT>(\n  target: T,\n  propertyKey: string | symbol,\n  descriptor: TypedPropertyDescriptor\u003Cany>\n) => void | TypedPropertyDescriptor\u003Cany>\n\n// 访问器装饰器（getter/setter）\ntype AccessorDecorator = \u003CT>(\n  target: T,\n  propertyKey: string | symbol,\n  descriptor: TypedPropertyDescriptor\u003Cany>\n) => void | TypedPropertyDescriptor\u003Cany>\n\n// 属性装饰器\ntype PropertyDecorator = (\n  target: Object,\n  propertyKey: string | symbol\n) => void\n",[73],[1943],{"type":27,"tag":35,"props":1944,"children":1945},{"__ignoreMap":7},[1946],{"type":32,"value":1940},{"type":27,"tag":28,"props":1948,"children":1949},{},[1950,1952,1958,1959,1965],{"type":32,"value":1951},"方法装饰器和属性装饰器虽然都接收 ",{"type":27,"tag":35,"props":1953,"children":1955},{"className":1954},[],[1956],{"type":32,"value":1957},"target",{"type":32,"value":139},{"type":27,"tag":35,"props":1960,"children":1962},{"className":1961},[],[1963],{"type":32,"value":1964},"propertyKey",{"type":32,"value":1966},"，但方法装饰器额外接收属性描述符（descriptor），所以可以修改方法的行为。属性装饰器没有 descriptor，只能做元数据附加。",{"type":27,"tag":59,"props":1968,"children":1970},{"id":1969},"模式-1自动日志方法装饰器",[1971],{"type":32,"value":1972},"模式 1：自动日志（方法装饰器）",{"type":27,"tag":28,"props":1974,"children":1975},{},[1976],{"type":32,"value":1977},"最常用的装饰器模式——给方法加日志，不侵入业务逻辑：",{"type":27,"tag":70,"props":1979,"children":1982},{"code":1980,"language":5,"meta":7,"className":1981},"function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value\n\n  descriptor.value = function (...args: any[]) {\n    console.log(`[${new Date().toISOString()}] ${propertyKey} called with:`, args)\n    const start = performance.now()\n    const result = originalMethod.apply(this, args)\n    const duration = performance.now() - start\n    console.log(`[${new Date().toISOString()}] ${propertyKey} completed in ${duration.toFixed(2)}ms`)\n    return result\n  }\n\n  return descriptor\n}\n\nclass UserService {\n  @log\n  async getUser(id: string) {\n    // 实际的业务逻辑\n    return db.users.find(id)\n  }\n\n  @log\n  async updateUser(id: string, data: Partial\u003CUser>) {\n    return db.users.update(id, data)\n  }\n}\n",[73],[1983],{"type":27,"tag":35,"props":1984,"children":1985},{"__ignoreMap":7},[1986],{"type":32,"value":1980},{"type":27,"tag":28,"props":1988,"children":1989},{},[1990],{"type":32,"value":1991},"输出：",{"type":27,"tag":70,"props":1993,"children":1995},{"code":1994},"[2026-06-04T10:00:00.000Z] getUser called with: [\"123\"]\n[2026-06-04T10:00:00.150Z] getUser completed in 150.23ms\n",[1996],{"type":27,"tag":35,"props":1997,"children":1998},{"__ignoreMap":7},[1999],{"type":32,"value":1994},{"type":27,"tag":28,"props":2001,"children":2002},{},[2003],{"type":32,"value":2004},"这种模式的优点是所有方法的日志行为一致，修改日志格式只需要改一个装饰器。而且每个装饰器是独立的——可以给重要方法加日志，给高频方法跳过。",{"type":27,"tag":59,"props":2006,"children":2008},{"id":2007},"模式-2输入校验方法装饰器-元数据",[2009],{"type":32,"value":2010},"模式 2：输入校验（方法装饰器 + 元数据）",{"type":27,"tag":28,"props":2012,"children":2013},{},[2014],{"type":32,"value":2015},"给方法参数加校验前置条件：",{"type":27,"tag":70,"props":2017,"children":2020},{"code":2018,"language":5,"meta":7,"className":2019},"type ValidationRule = (value: any) => string | null // null 表示通过\n\nfunction validate(rules: Record\u003Cstring, ValidationRule>) {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    const originalMethod = descriptor.value\n\n    descriptor.value = function (...args: any[]) {\n      // 通过参数名匹配规则（需要额外元数据来映射参数名）\n      const paramNames = getParamNames(originalMethod)\n      for (const [name, rule] of Object.entries(rules)) {\n        const index = paramNames.indexOf(name)\n        if (index === -1) continue\n        const error = rule(args[index])\n        if (error !== null) {\n          throw new ValidationError(`Validation failed for ${propertyKey}.${name}: ${error}`)\n        }\n      }\n\n      return originalMethod.apply(this, args)\n    }\n\n    return descriptor\n  }\n}\n\nclass UserController {\n  @validate({\n    email: (v: any) => typeof v === 'string' && v.includes('@') ? null : 'Invalid email',\n    age: (v: any) => typeof v === 'number' && v >= 0 && v \u003C= 150 ? null : 'Invalid age'\n  })\n  async createUser(email: string, age: number) {\n    // ...\n  }\n}\n",[73],[2021],{"type":27,"tag":35,"props":2022,"children":2023},{"__ignoreMap":7},[2024],{"type":32,"value":2018},{"type":27,"tag":28,"props":2026,"children":2027},{},[2028],{"type":32,"value":2029},"这里不需要在每个方法体里手写校验逻辑。如果将来校验规则变了，只需要改装饰器参数。",{"type":27,"tag":59,"props":2031,"children":2033},{"id":2032},"模式-3方法节流防抖方法装饰器",[2034],{"type":32,"value":2035},"模式 3：方法节流/防抖（方法装饰器）",{"type":27,"tag":28,"props":2037,"children":2038},{},[2039],{"type":32,"value":2040},"在 UI 事件或 API 调用场景下，节流和防抖是常见需求：",{"type":27,"tag":70,"props":2042,"children":2045},{"code":2043,"language":5,"meta":7,"className":2044},"function throttle(ms: number) {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    const originalMethod = descriptor.value\n    let lastCall = 0\n\n    descriptor.value = function (...args: any[]) {\n      const now = Date.now()\n      if (now - lastCall \u003C ms) return\n      lastCall = now\n      return originalMethod.apply(this, args)\n    }\n\n    return descriptor\n  }\n}\n\nclass AnalyticsTracker {\n  @throttle(1000) // 每秒最多上报一次\n  trackEvent(name: string, data: Record\u003Cstring, any>) {\n    // 发送分析事件\n    navigator.sendBeacon('/api/analytics', JSON.stringify({ name, data }))\n  }\n}\n",[73],[2046],{"type":27,"tag":35,"props":2047,"children":2048},{"__ignoreMap":7},[2049],{"type":32,"value":2043},{"type":27,"tag":59,"props":2051,"children":2053},{"id":2052},"模式-4依赖注入类装饰器-属性装饰器",[2054],{"type":32,"value":2055},"模式 4：依赖注入（类装饰器 + 属性装饰器）",{"type":27,"tag":28,"props":2057,"children":2058},{},[2059],{"type":32,"value":2060},"依赖注入是类级别装饰器的典型应用。先定义一个容器：",{"type":27,"tag":70,"props":2062,"children":2065},{"code":2063,"language":5,"meta":7,"className":2064},"const container = new Map\u003Cstring, any>()\n\nfunction Injectable() {\n  return function (target: any) {\n    // 标记这个类可以被注入\n    Reflect.defineMetadata('injectable', true, target)\n  }\n}\n\nfunction Inject(token: string) {\n  return function (target: any, propertyKey: string | symbol) {\n    // 在属性上存储依赖标记\n    Reflect.defineMetadata('inject', token, target, propertyKey)\n  }\n}\n\n// 自动装配\nfunction autoInject\u003CT extends { new (...args: any[]): any }>(target: T): T {\n  return class extends target {\n    constructor(...args: any[]) {\n      super(...args)\n      // 遍历原型链，查找需要注入的属性\n      const instance = this as any\n      const proto = Object.getPrototypeOf(instance)\n      for (const key of Object.getOwnPropertyNames(proto)) {\n        const token = Reflect.getMetadata('inject', proto, key)\n        if (token && container.has(token)) {\n          instance[key] = container.get(token)\n        }\n      }\n    }\n  }\n}\n\n@Injectable()\nclass Logger {\n  log(msg: string) { console.log(msg) }\n}\n\n@Injectable()\n@autoInject\nclass UserServiceDI {\n  @Inject('Logger')\n  private logger!: Logger\n\n  async getUser(id: string) {\n    this.logger.log(`Fetching user ${id}`)\n    // ...\n  }\n}\n",[73],[2066],{"type":27,"tag":35,"props":2067,"children":2068},{"__ignoreMap":7},[2069],{"type":32,"value":2063},{"type":27,"tag":28,"props":2071,"children":2072},{},[2073,2075,2081],{"type":32,"value":2074},"实际项目一般用 ",{"type":27,"tag":35,"props":2076,"children":2078},{"className":2077},[],[2079],{"type":32,"value":2080},"reflect-metadata",{"type":32,"value":2082}," 库来处理元数据操作。这里的关键是：属性装饰器只能标记\"需要注入什么\"，具体的注入逻辑在类装饰器（autoInject）中执行。",{"type":27,"tag":59,"props":2084,"children":2086},{"id":2085},"传统装饰器-vs-标准装饰器",[2087],{"type":32,"value":2088},"传统装饰器 vs 标准装饰器",{"type":27,"tag":28,"props":2090,"children":2091},{},[2092],{"type":32,"value":2093},"TypeScript 5.0 开始支持 ECMAScript 标准装饰器。主要区别：",{"type":27,"tag":552,"props":2095,"children":2096},{},[2097,2118],{"type":27,"tag":556,"props":2098,"children":2099},{},[2100],{"type":27,"tag":560,"props":2101,"children":2102},{},[2103,2108,2113],{"type":27,"tag":564,"props":2104,"children":2105},{},[2106],{"type":32,"value":2107},"方面",{"type":27,"tag":564,"props":2109,"children":2110},{},[2111],{"type":32,"value":2112},"传统装饰器（experimental）",{"type":27,"tag":564,"props":2114,"children":2115},{},[2116],{"type":32,"value":2117},"标准装饰器（Standard）",{"type":27,"tag":580,"props":2119,"children":2120},{},[2121,2144,2170,2188,2209],{"type":27,"tag":560,"props":2122,"children":2123},{},[2124,2129,2139],{"type":27,"tag":587,"props":2125,"children":2126},{},[2127],{"type":32,"value":2128},"tsconfig 配置",{"type":27,"tag":587,"props":2130,"children":2131},{},[2132,2134],{"type":32,"value":2133},"需要 ",{"type":27,"tag":35,"props":2135,"children":2137},{"className":2136},[],[2138],{"type":32,"value":1920},{"type":27,"tag":587,"props":2140,"children":2141},{},[2142],{"type":32,"value":2143},"不需要",{"type":27,"tag":560,"props":2145,"children":2146},{},[2147,2152,2161],{"type":27,"tag":587,"props":2148,"children":2149},{},[2150],{"type":32,"value":2151},"方法装饰器参数",{"type":27,"tag":587,"props":2153,"children":2154},{},[2155],{"type":27,"tag":35,"props":2156,"children":2158},{"className":2157},[],[2159],{"type":32,"value":2160},"(target, key, descriptor)",{"type":27,"tag":587,"props":2162,"children":2163},{},[2164],{"type":27,"tag":35,"props":2165,"children":2167},{"className":2166},[],[2168],{"type":32,"value":2169},"(target, context)",{"type":27,"tag":560,"props":2171,"children":2172},{},[2173,2178,2183],{"type":27,"tag":587,"props":2174,"children":2175},{},[2176],{"type":32,"value":2177},"返回值",{"type":27,"tag":587,"props":2179,"children":2180},{},[2181],{"type":32,"value":2182},"descriptor 或无",{"type":27,"tag":587,"props":2184,"children":2185},{},[2186],{"type":32,"value":2187},"新函数或 void",{"type":27,"tag":560,"props":2189,"children":2190},{},[2191,2196,2204],{"type":27,"tag":587,"props":2192,"children":2193},{},[2194],{"type":32,"value":2195},"元数据支持",{"type":27,"tag":587,"props":2197,"children":2198},{},[2199],{"type":27,"tag":35,"props":2200,"children":2202},{"className":2201},[],[2203],{"type":32,"value":2080},{"type":27,"tag":587,"props":2205,"children":2206},{},[2207],{"type":32,"value":2208},"需要装饰器自行管理",{"type":27,"tag":560,"props":2210,"children":2211},{},[2212,2217,2222],{"type":27,"tag":587,"props":2213,"children":2214},{},[2215],{"type":32,"value":2216},"与 Babel 兼容",{"type":27,"tag":587,"props":2218,"children":2219},{},[2220],{"type":32,"value":2221},"不一致",{"type":27,"tag":587,"props":2223,"children":2224},{},[2225],{"type":32,"value":2226},"一致",{"type":27,"tag":28,"props":2228,"children":2229},{},[2230],{"type":32,"value":2231},"标准装饰器的方法装饰器签名：",{"type":27,"tag":70,"props":2233,"children":2236},{"code":2234,"language":5,"meta":7,"className":2235},"function logged\u003CT extends (...args: any[]) => any>(\n  target: (this: void, ...args: Parameters\u003CT>) => ReturnType\u003CT>,\n  context: ClassMethodDecoratorContext\n) {\n  const methodName = String(context.name)\n\n  function replacementMethod(this: any, ...args: any[]) {\n    console.log(`LOG: Entering method '${methodName}'.`)\n    const result = target.call(this, ...args)\n    console.log(`LOG: Exiting method '${methodName}'.`)\n    return result\n  }\n\n  return replacementMethod\n}\n",[73],[2237],{"type":27,"tag":35,"props":2238,"children":2239},{"__ignoreMap":7},[2240],{"type":32,"value":2234},{"type":27,"tag":28,"props":2242,"children":2243},{},[2244,2246,2252,2253,2259,2260,2266,2267,2273,2274,2280],{"type":32,"value":2245},"context 对象提供了 ",{"type":27,"tag":35,"props":2247,"children":2249},{"className":2248},[],[2250],{"type":32,"value":2251},"name",{"type":32,"value":42},{"type":27,"tag":35,"props":2254,"children":2256},{"className":2255},[],[2257],{"type":32,"value":2258},"kind",{"type":32,"value":42},{"type":27,"tag":35,"props":2261,"children":2263},{"className":2262},[],[2264],{"type":32,"value":2265},"static",{"type":32,"value":42},{"type":27,"tag":35,"props":2268,"children":2270},{"className":2269},[],[2271],{"type":32,"value":2272},"private",{"type":32,"value":42},{"type":27,"tag":35,"props":2275,"children":2277},{"className":2276},[],[2278],{"type":32,"value":2279},"addInitializer",{"type":32,"value":2281}," 等信息，比传统装饰器的签名更丰富。",{"type":27,"tag":59,"props":2283,"children":2284},{"id":723},[2285],{"type":32,"value":723},{"type":27,"tag":552,"props":2287,"children":2288},{},[2289,2310],{"type":27,"tag":556,"props":2290,"children":2291},{},[2292],{"type":27,"tag":560,"props":2293,"children":2294},{},[2295,2300,2305],{"type":27,"tag":564,"props":2296,"children":2297},{},[2298],{"type":32,"value":2299},"装饰器类型",{"type":27,"tag":564,"props":2301,"children":2302},{},[2303],{"type":32,"value":2304},"能做的事",{"type":27,"tag":564,"props":2306,"children":2307},{},[2308],{"type":32,"value":2309},"典型用途",{"type":27,"tag":580,"props":2311,"children":2312},{},[2313,2331,2349,2367],{"type":27,"tag":560,"props":2314,"children":2315},{},[2316,2321,2326],{"type":27,"tag":587,"props":2317,"children":2318},{},[2319],{"type":32,"value":2320},"类装饰器",{"type":27,"tag":587,"props":2322,"children":2323},{},[2324],{"type":32,"value":2325},"替换或包装构造函数",{"type":27,"tag":587,"props":2327,"children":2328},{},[2329],{"type":32,"value":2330},"依赖注入、注册到框架",{"type":27,"tag":560,"props":2332,"children":2333},{},[2334,2339,2344],{"type":27,"tag":587,"props":2335,"children":2336},{},[2337],{"type":32,"value":2338},"方法装饰器",{"type":27,"tag":587,"props":2340,"children":2341},{},[2342],{"type":32,"value":2343},"修改方法行为",{"type":27,"tag":587,"props":2345,"children":2346},{},[2347],{"type":32,"value":2348},"日志、校验、重试、缓存",{"type":27,"tag":560,"props":2350,"children":2351},{},[2352,2357,2362],{"type":27,"tag":587,"props":2353,"children":2354},{},[2355],{"type":32,"value":2356},"访问器装饰器",{"type":27,"tag":587,"props":2358,"children":2359},{},[2360],{"type":32,"value":2361},"修改 getter/setter",{"type":27,"tag":587,"props":2363,"children":2364},{},[2365],{"type":32,"value":2366},"响应式计算、延迟初始化",{"type":27,"tag":560,"props":2368,"children":2369},{},[2370,2375,2380],{"type":27,"tag":587,"props":2371,"children":2372},{},[2373],{"type":32,"value":2374},"属性装饰器",{"type":27,"tag":587,"props":2376,"children":2377},{},[2378],{"type":32,"value":2379},"附加元数据",{"type":27,"tag":587,"props":2381,"children":2382},{},[2383],{"type":32,"value":2384},"注入标记、序列化映射",{"type":27,"tag":28,"props":2386,"children":2387},{},[2388],{"type":32,"value":2389},"装饰器的核心价值在于横切关注点（cross-cutting concerns）的分离。日志、校验、权限检查这类逻辑不适合散落在每个方法体内——它们横跨多个方法，最好用一个装饰器统一管理。使用装饰器时要注意的一个原则是：它应该增强方法但不改变方法的语义，否则会引入难以调试的隐式行为。",{"title":7,"searchDepth":732,"depth":732,"links":2391},[2392,2393,2394,2395,2396,2397,2398],{"id":1930,"depth":735,"text":1930},{"id":1969,"depth":735,"text":1972},{"id":2007,"depth":735,"text":2010},{"id":2032,"depth":735,"text":2035},{"id":2052,"depth":735,"text":2055},{"id":2085,"depth":735,"text":2088},{"id":723,"depth":735,"text":723},"content:topics:typescript:typescript-decorators-real-world-validation-logging-di.md","topics/typescript/typescript-decorators-real-world-validation-logging-di.md","topics/typescript/typescript-decorators-real-world-validation-logging-di",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":2403,"image":18,"imageQuery":19,"pexelsPhotoId":20,"pexelsUrl":21,"featured":6,"readingTime":22,"body":2404,"_type":753,"_id":754,"_source":755,"_file":756,"_stem":757,"_extension":758},[13,14,15,16,17],{"type":24,"children":2405,"toc":2972},[2406,2428,2432,2436,2444,2459,2463,2471,2510,2514,2524,2532,2542,2579,2589,2593,2608,2616,2631,2635,2643,2647,2651,2659,2663,2667,2675,2679,2687,2691,2695,2703,2731,2735,2739,2747,2769,2773,2781,2791,2795,2799,2807,2811,2819,2823,2964,2968],{"type":27,"tag":28,"props":2407,"children":2408},{},[2409,2410,2415,2416,2421,2422,2427],{"type":32,"value":33},{"type":27,"tag":35,"props":2411,"children":2413},{"className":2412},[],[2414],{"type":32,"value":40},{"type":32,"value":42},{"type":27,"tag":35,"props":2417,"children":2419},{"className":2418},[],[2420],{"type":32,"value":48},{"type":32,"value":42},{"type":27,"tag":35,"props":2423,"children":2425},{"className":2424},[],[2426],{"type":32,"value":55},{"type":32,"value":57},{"type":27,"tag":59,"props":2429,"children":2430},{"id":61},[2431],{"type":32,"value":61},{"type":27,"tag":28,"props":2433,"children":2434},{},[2435],{"type":32,"value":68},{"type":27,"tag":70,"props":2437,"children":2439},{"className":2438,"code":74,"language":5,"meta":7},[73],[2440],{"type":27,"tag":35,"props":2441,"children":2442},{"__ignoreMap":7},[2443],{"type":32,"value":74},{"type":27,"tag":28,"props":2445,"children":2446},{},[2447,2452,2453,2458],{"type":27,"tag":35,"props":2448,"children":2450},{"className":2449},[],[2451],{"type":32,"value":88},{"type":32,"value":90},{"type":27,"tag":35,"props":2454,"children":2456},{"className":2455},[],[2457],{"type":32,"value":96},{"type":32,"value":98},{"type":27,"tag":28,"props":2460,"children":2461},{},[2462],{"type":32,"value":103},{"type":27,"tag":70,"props":2464,"children":2466},{"className":2465,"code":107,"language":5,"meta":7},[73],[2467],{"type":27,"tag":35,"props":2468,"children":2469},{"__ignoreMap":7},[2470],{"type":32,"value":107},{"type":27,"tag":28,"props":2472,"children":2473},{},[2474,2479,2480,2485,2486,2491,2492,2497,2498,2503,2504,2509],{"type":27,"tag":35,"props":2475,"children":2477},{"className":2476},[],[2478],{"type":32,"value":121},{"type":32,"value":123},{"type":27,"tag":35,"props":2481,"children":2483},{"className":2482},[],[2484],{"type":32,"value":129},{"type":32,"value":131},{"type":27,"tag":35,"props":2487,"children":2489},{"className":2488},[],[2490],{"type":32,"value":137},{"type":32,"value":139},{"type":27,"tag":35,"props":2493,"children":2495},{"className":2494},[],[2496],{"type":32,"value":145},{"type":32,"value":147},{"type":27,"tag":35,"props":2499,"children":2501},{"className":2500},[],[2502],{"type":32,"value":48},{"type":32,"value":139},{"type":27,"tag":35,"props":2505,"children":2507},{"className":2506},[],[2508],{"type":32,"value":159},{"type":32,"value":161},{"type":27,"tag":59,"props":2511,"children":2512},{"id":164},[2513],{"type":32,"value":167},{"type":27,"tag":28,"props":2515,"children":2516},{},[2517,2518,2523],{"type":32,"value":172},{"type":27,"tag":35,"props":2519,"children":2521},{"className":2520},[],[2522],{"type":32,"value":178},{"type":32,"value":180},{"type":27,"tag":70,"props":2525,"children":2527},{"className":2526,"code":184,"language":5,"meta":7},[73],[2528],{"type":27,"tag":35,"props":2529,"children":2530},{"__ignoreMap":7},[2531],{"type":32,"value":184},{"type":27,"tag":28,"props":2533,"children":2534},{},[2535,2536,2541],{"type":32,"value":194},{"type":27,"tag":35,"props":2537,"children":2539},{"className":2538},[],[2540],{"type":32,"value":200},{"type":32,"value":202},{"type":27,"tag":204,"props":2543,"children":2544},{},[2545,2560,2569],{"type":27,"tag":208,"props":2546,"children":2547},{},[2548,2553,2554,2559],{"type":27,"tag":35,"props":2549,"children":2551},{"className":2550},[],[2552],{"type":32,"value":216},{"type":32,"value":218},{"type":27,"tag":35,"props":2555,"children":2557},{"className":2556},[],[2558],{"type":32,"value":224},{"type":32,"value":226},{"type":27,"tag":208,"props":2561,"children":2562},{},[2563,2568],{"type":27,"tag":35,"props":2564,"children":2566},{"className":2565},[],[2567],{"type":32,"value":235},{"type":32,"value":237},{"type":27,"tag":208,"props":2570,"children":2571},{},[2572,2573,2578],{"type":32,"value":242},{"type":27,"tag":35,"props":2574,"children":2576},{"className":2575},[],[2577],{"type":32,"value":248},{"type":32,"value":250},{"type":27,"tag":28,"props":2580,"children":2581},{},[2582,2583,2588],{"type":32,"value":255},{"type":27,"tag":35,"props":2584,"children":2586},{"className":2585},[],[2587],{"type":32,"value":178},{"type":32,"value":262},{"type":27,"tag":264,"props":2590,"children":2591},{"id":266},[2592],{"type":32,"value":266},{"type":27,"tag":28,"props":2594,"children":2595},{},[2596,2601,2602,2607],{"type":27,"tag":35,"props":2597,"children":2599},{"className":2598},[],[2600],{"type":32,"value":178},{"type":32,"value":278},{"type":27,"tag":35,"props":2603,"children":2605},{"className":2604},[],[2606],{"type":32,"value":284},{"type":32,"value":286},{"type":27,"tag":70,"props":2609,"children":2611},{"className":2610,"code":290,"language":5,"meta":7},[73],[2612],{"type":27,"tag":35,"props":2613,"children":2614},{"__ignoreMap":7},[2615],{"type":32,"value":290},{"type":27,"tag":28,"props":2617,"children":2618},{},[2619,2624,2625,2630],{"type":27,"tag":35,"props":2620,"children":2622},{"className":2621},[],[2623],{"type":32,"value":304},{"type":32,"value":306},{"type":27,"tag":35,"props":2626,"children":2628},{"className":2627},[],[2629],{"type":32,"value":284},{"type":32,"value":313},{"type":27,"tag":264,"props":2632,"children":2633},{"id":316},[2634],{"type":32,"value":316},{"type":27,"tag":70,"props":2636,"children":2638},{"className":2637,"code":322,"language":5,"meta":7},[73],[2639],{"type":27,"tag":35,"props":2640,"children":2641},{"__ignoreMap":7},[2642],{"type":32,"value":322},{"type":27,"tag":264,"props":2644,"children":2645},{"id":330},[2646],{"type":32,"value":330},{"type":27,"tag":28,"props":2648,"children":2649},{},[2650],{"type":32,"value":337},{"type":27,"tag":70,"props":2652,"children":2654},{"className":2653,"code":341,"language":5,"meta":7},[73],[2655],{"type":27,"tag":35,"props":2656,"children":2657},{"__ignoreMap":7},[2658],{"type":32,"value":341},{"type":27,"tag":59,"props":2660,"children":2661},{"id":349},[2662],{"type":32,"value":349},{"type":27,"tag":264,"props":2664,"children":2665},{"id":354},[2666],{"type":32,"value":357},{"type":27,"tag":70,"props":2668,"children":2670},{"className":2669,"code":361,"language":5,"meta":7},[73],[2671],{"type":27,"tag":35,"props":2672,"children":2673},{"__ignoreMap":7},[2674],{"type":32,"value":361},{"type":27,"tag":264,"props":2676,"children":2677},{"id":369},[2678],{"type":32,"value":372},{"type":27,"tag":70,"props":2680,"children":2682},{"className":2681,"code":376,"language":5,"meta":7},[73],[2683],{"type":27,"tag":35,"props":2684,"children":2685},{"__ignoreMap":7},[2686],{"type":32,"value":376},{"type":27,"tag":28,"props":2688,"children":2689},{},[2690],{"type":32,"value":386},{"type":27,"tag":264,"props":2692,"children":2693},{"id":389},[2694],{"type":32,"value":392},{"type":27,"tag":70,"props":2696,"children":2698},{"className":2697,"code":396,"language":5,"meta":7},[73],[2699],{"type":27,"tag":35,"props":2700,"children":2701},{"__ignoreMap":7},[2702],{"type":32,"value":396},{"type":27,"tag":28,"props":2704,"children":2705},{},[2706,2707,2712,2713,2718,2719,2724,2725,2730],{"type":32,"value":406},{"type":27,"tag":35,"props":2708,"children":2710},{"className":2709},[],[2711],{"type":32,"value":137},{"type":32,"value":413},{"type":27,"tag":35,"props":2714,"children":2716},{"className":2715},[],[2717],{"type":32,"value":419},{"type":32,"value":421},{"type":27,"tag":35,"props":2720,"children":2722},{"className":2721},[],[2723],{"type":32,"value":427},{"type":32,"value":139},{"type":27,"tag":35,"props":2726,"children":2728},{"className":2727},[],[2729],{"type":32,"value":434},{"type":32,"value":436},{"type":27,"tag":59,"props":2732,"children":2733},{"id":439},[2734],{"type":32,"value":442},{"type":27,"tag":264,"props":2736,"children":2737},{"id":445},[2738],{"type":32,"value":448},{"type":27,"tag":70,"props":2740,"children":2742},{"className":2741,"code":452,"language":5,"meta":7},[73],[2743],{"type":27,"tag":35,"props":2744,"children":2745},{"__ignoreMap":7},[2746],{"type":32,"value":452},{"type":27,"tag":28,"props":2748,"children":2749},{},[2750,2751,2756,2757,2762,2763,2768],{"type":32,"value":462},{"type":27,"tag":35,"props":2752,"children":2754},{"className":2753},[],[2755],{"type":32,"value":216},{"type":32,"value":469},{"type":27,"tag":35,"props":2758,"children":2760},{"className":2759},[],[2761],{"type":32,"value":224},{"type":32,"value":476},{"type":27,"tag":35,"props":2764,"children":2766},{"className":2765},[],[2767],{"type":32,"value":284},{"type":32,"value":436},{"type":27,"tag":264,"props":2770,"children":2771},{"id":485},[2772],{"type":32,"value":488},{"type":27,"tag":70,"props":2774,"children":2776},{"className":2775,"code":492,"language":5,"meta":7},[73],[2777],{"type":27,"tag":35,"props":2778,"children":2779},{"__ignoreMap":7},[2780],{"type":32,"value":492},{"type":27,"tag":28,"props":2782,"children":2783},{},[2784,2785,2790],{"type":32,"value":502},{"type":27,"tag":35,"props":2786,"children":2788},{"className":2787},[],[2789],{"type":32,"value":508},{"type":32,"value":510},{"type":27,"tag":264,"props":2792,"children":2793},{"id":513},[2794],{"type":32,"value":516},{"type":27,"tag":28,"props":2796,"children":2797},{},[2798],{"type":32,"value":521},{"type":27,"tag":70,"props":2800,"children":2802},{"className":2801,"code":525,"language":5,"meta":7},[73],[2803],{"type":27,"tag":35,"props":2804,"children":2805},{"__ignoreMap":7},[2806],{"type":32,"value":525},{"type":27,"tag":28,"props":2808,"children":2809},{},[2810],{"type":32,"value":535},{"type":27,"tag":70,"props":2812,"children":2814},{"className":2813,"code":539,"language":5,"meta":7},[73],[2815],{"type":27,"tag":35,"props":2816,"children":2817},{"__ignoreMap":7},[2818],{"type":32,"value":539},{"type":27,"tag":59,"props":2820,"children":2821},{"id":547},[2822],{"type":32,"value":550},{"type":27,"tag":552,"props":2824,"children":2825},{},[2826,2844],{"type":27,"tag":556,"props":2827,"children":2828},{},[2829],{"type":27,"tag":560,"props":2830,"children":2831},{},[2832,2836,2840],{"type":27,"tag":564,"props":2833,"children":2834},{},[2835],{"type":32,"value":568},{"type":27,"tag":564,"props":2837,"children":2838},{},[2839],{"type":32,"value":573},{"type":27,"tag":564,"props":2841,"children":2842},{},[2843],{"type":32,"value":578},{"type":27,"tag":580,"props":2845,"children":2846},{},[2847,2869,2891,2911,2930,2945],{"type":27,"tag":560,"props":2848,"children":2849},{},[2850,2854,2862],{"type":27,"tag":587,"props":2851,"children":2852},{},[2853],{"type":32,"value":591},{"type":27,"tag":587,"props":2855,"children":2856},{},[2857],{"type":27,"tag":35,"props":2858,"children":2860},{"className":2859},[],[2861],{"type":32,"value":600},{"type":27,"tag":587,"props":2863,"children":2864},{},[2865,2866],{"type":32,"value":605},{"type":27,"tag":607,"props":2867,"children":2868},{},[],{"type":27,"tag":560,"props":2870,"children":2871},{},[2872,2876,2884],{"type":27,"tag":587,"props":2873,"children":2874},{},[2875],{"type":32,"value":617},{"type":27,"tag":587,"props":2877,"children":2878},{},[2879],{"type":27,"tag":35,"props":2880,"children":2882},{"className":2881},[],[2883],{"type":32,"value":626},{"type":27,"tag":587,"props":2885,"children":2886},{},[2887,2888],{"type":32,"value":631},{"type":27,"tag":607,"props":2889,"children":2890},{},[],{"type":27,"tag":560,"props":2892,"children":2893},{},[2894,2898,2907],{"type":27,"tag":587,"props":2895,"children":2896},{},[2897],{"type":32,"value":642},{"type":27,"tag":587,"props":2899,"children":2900},{},[2901,2906],{"type":27,"tag":35,"props":2902,"children":2904},{"className":2903},[],[2905],{"type":32,"value":651},{"type":32,"value":653},{"type":27,"tag":587,"props":2908,"children":2909},{},[2910],{"type":32,"value":658},{"type":27,"tag":560,"props":2912,"children":2913},{},[2914,2918,2926],{"type":27,"tag":587,"props":2915,"children":2916},{},[2917],{"type":32,"value":666},{"type":27,"tag":587,"props":2919,"children":2920},{},[2921],{"type":27,"tag":35,"props":2922,"children":2924},{"className":2923},[],[2925],{"type":32,"value":675},{"type":27,"tag":587,"props":2927,"children":2928},{},[2929],{"type":32,"value":680},{"type":27,"tag":560,"props":2931,"children":2932},{},[2933,2937,2941],{"type":27,"tag":587,"props":2934,"children":2935},{},[2936],{"type":32,"value":688},{"type":27,"tag":587,"props":2938,"children":2939},{},[2940],{"type":32,"value":693},{"type":27,"tag":587,"props":2942,"children":2943},{},[2944],{"type":32,"value":698},{"type":27,"tag":560,"props":2946,"children":2947},{},[2948,2952,2960],{"type":27,"tag":587,"props":2949,"children":2950},{},[2951],{"type":32,"value":706},{"type":27,"tag":587,"props":2953,"children":2954},{},[2955],{"type":27,"tag":35,"props":2956,"children":2958},{"className":2957},[],[2959],{"type":32,"value":715},{"type":27,"tag":587,"props":2961,"children":2962},{},[2963],{"type":32,"value":720},{"type":27,"tag":59,"props":2965,"children":2966},{"id":723},[2967],{"type":32,"value":723},{"type":27,"tag":28,"props":2969,"children":2970},{},[2971],{"type":32,"value":730},{"title":7,"searchDepth":732,"depth":732,"links":2973},[2974,2975,2980,2985,2990,2991],{"id":61,"depth":735,"text":61},{"id":164,"depth":735,"text":167,"children":2976},[2977,2978,2979],{"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":2981},[2982,2983,2984],{"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":2986},[2987,2988,2989],{"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},1780533153807]