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