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