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