[{"data":1,"prerenderedAt":3034},["ShallowReactive",2],{"article-/topics/typescript/typescript-discriminated-unions-exhaustive-check-state-management":3,"related-typescript":919,"content-query-58MDusjVUr":2321},{"_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,1785],{"_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":1362,"topic":5,"author":11,"tags":1363,"image":1368,"imageQuery":1369,"pexelsPhotoId":1370,"pexelsUrl":1371,"featured":6,"readingTime":1372,"body":1373,"_type":913,"_id":1782,"_source":915,"_file":1783,"_stem":1784,"_extension":918},"/topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy","TypeScript 设计模式实战：工厂、策略、适配器与代理怎样保持类型安全","设计模式在 TypeScript 里不该变成 class 套娃。本文从工厂、策略、适配器和代理四个高频场景出发，讲清如何让抽象保持灵活，同时不牺牲类型安全和可读性。","2026-06-08",[13,1364,1365,1366,1367],"Design Patterns","Factory Pattern","Strategy Pattern","Adapter Pattern","/images/articles/typescript-design-patterns-factory-strategy-adapter-proxy-featured.jpg","software design patterns code laptop",34804023,"https://www.pexels.com/photo/close-up-of-computer-screen-with-code-reflection-34804023/",17,{"type":24,"children":1374,"toc":1773},[1375,1380,1393,1399,1404,1415,1428,1434,1439,1448,1460,1466,1487,1496,1501,1507,1512,1521,1526,1532,1537,1572,1577,1583,1682,1686,1691,1696,1732,1737],{"type":27,"tag":28,"props":1376,"children":1377},{},[1378],{"type":32,"value":1379},"很多人一提到“设计模式 + TypeScript”，脑子里马上冒出一套厚重的 class 结构：抽象工厂、基类、接口、子类、再来几层继承。问题在于，前端和 Node 项目里真正需要的，往往不是把经典 OO 图谱原样搬进来，而是把“变化点”抽象出来，同时保证调用方还能读懂类型。",{"type":27,"tag":28,"props":1381,"children":1382},{},[1383,1385,1391],{"type":32,"value":1384},"TypeScript 在设计模式里的价值，不是让模式更像 Java，而是让模式更诚实。哪些输入是合法的、哪些策略实现必须覆盖、适配器是否真的完成字段转换、代理是否保留原函数签名，这些都可以由类型系统提前约束。如果模式一上来就把类型信息打成 ",{"type":27,"tag":40,"props":1386,"children":1388},{"className":1387},[],[1389],{"type":32,"value":1390},"any",{"type":32,"value":1392},"，那它带来的通常不是灵活，而是延后的风险。",{"type":27,"tag":49,"props":1394,"children":1396},{"id":1395},"工厂模式把怎么创建隔离出来但别把输入做成万能配置桶",[1397],{"type":32,"value":1398},"工厂模式：把“怎么创建”隔离出来，但别把输入做成万能配置桶",{"type":27,"tag":28,"props":1400,"children":1401},{},[1402],{"type":32,"value":1403},"工厂模式最适合解决创建逻辑和环境差异问题，比如：不同运行环境要创建不同客户端，不同支付方式要创建不同处理器。",{"type":27,"tag":96,"props":1405,"children":1410},{"className":1406,"code":1408,"language":1409,"meta":7},[1407],"language-ts","type StorageKind = 'memory' | 'redis'\n\ninterface Storage {\n  get(key: string): Promise\u003Cstring | null>\n  set(key: string, value: string): Promise\u003Cvoid>\n}\n\nfunction createStorage(kind: StorageKind): Storage {\n  switch (kind) {\n    case 'memory':\n      return createMemoryStorage()\n    case 'redis':\n      return createRedisStorage()\n  }\n}\n","ts",[1411],{"type":27,"tag":40,"props":1412,"children":1413},{"__ignoreMap":7},[1414],{"type":32,"value":1408},{"type":27,"tag":28,"props":1416,"children":1417},{},[1418,1420,1426],{"type":32,"value":1419},"真正要避免的，不是工厂本身，而是把工厂入参做成一个什么都能塞的 ",{"type":27,"tag":40,"props":1421,"children":1423},{"className":1422},[],[1424],{"type":32,"value":1425},"Record\u003Cstring, any>",{"type":32,"value":1427},"。工厂模式一旦把输入边界放宽，调用方就会失去类型提示，后面连工厂是否真的支持某个组合都看不出来。",{"type":27,"tag":49,"props":1429,"children":1431},{"id":1430},"策略模式别用-if-else-链拖着业务跑把变化面显式枚举出来",[1432],{"type":32,"value":1433},"策略模式：别用 if else 链拖着业务跑，把变化面显式枚举出来",{"type":27,"tag":28,"props":1435,"children":1436},{},[1437],{"type":32,"value":1438},"策略模式最值钱的地方，是把“可替换规则”显式列出来，让新增策略变成扩展而不是改旧逻辑。",{"type":27,"tag":96,"props":1440,"children":1443},{"className":1441,"code":1442,"language":1409,"meta":7},[1407],"type PricingStrategy = 'standard' | 'vip' | 'promotion'\n\nconst pricingMap: Record\u003CPricingStrategy, (price: number) => number> = {\n  standard: (price) => price,\n  vip: (price) => price * 0.9,\n  promotion: (price) => price - 30\n}\n\nfunction calcPrice(strategy: PricingStrategy, basePrice: number): number {\n  return pricingMap[strategy](basePrice)\n}\n",[1444],{"type":27,"tag":40,"props":1445,"children":1446},{"__ignoreMap":7},[1447],{"type":32,"value":1442},{"type":27,"tag":28,"props":1449,"children":1450},{},[1451,1452,1458],{"type":32,"value":618},{"type":27,"tag":40,"props":1453,"children":1455},{"className":1454},[],[1456],{"type":32,"value":1457},"Record\u003CPricingStrategy, ...>",{"type":32,"value":1459}," 的意义很大：只要你新增一个策略名，TypeScript 会强制你把实现补齐。这样策略模式不只是结构好看，而是具备穷举约束。",{"type":27,"tag":49,"props":1461,"children":1463},{"id":1462},"适配器模式真正重要的是把外部不稳定结构拦在边界外",[1464],{"type":32,"value":1465},"适配器模式：真正重要的是把外部不稳定结构拦在边界外",{"type":27,"tag":28,"props":1467,"children":1468},{},[1469,1471,1477,1479,1485],{"type":32,"value":1470},"适配器最常被低估。很多团队明明已经接了多个第三方服务，却还在业务层到处判断“这个平台字段叫 ",{"type":27,"tag":40,"props":1472,"children":1474},{"className":1473},[],[1475],{"type":32,"value":1476},"full_name",{"type":32,"value":1478},"，那个平台叫 ",{"type":27,"tag":40,"props":1480,"children":1482},{"className":1481},[],[1483],{"type":32,"value":1484},"displayName",{"type":32,"value":1486},"”。本质上，这就是缺了适配器层。",{"type":27,"tag":96,"props":1488,"children":1491},{"className":1489,"code":1490,"language":1409,"meta":7},[1407],"type VendorUser = {\n  uid: string\n  full_name: string\n  active_flag: 0 | 1\n}\n\ntype UserProfile = {\n  id: string\n  name: string\n  isActive: boolean\n}\n\nfunction adaptVendorUser(input: VendorUser): UserProfile {\n  return {\n    id: input.uid,\n    name: input.full_name,\n    isActive: input.active_flag === 1\n  }\n}\n",[1492],{"type":27,"tag":40,"props":1493,"children":1494},{"__ignoreMap":7},[1495],{"type":32,"value":1490},{"type":27,"tag":28,"props":1497,"children":1498},{},[1499],{"type":32,"value":1500},"适配器模式的关键，不是写一个转换函数，而是把外部世界的不稳定命名和字段结构限制在边界里，别让业务层长期直接面对这些差异。",{"type":27,"tag":49,"props":1502,"children":1504},{"id":1503},"代理模式包装额外行为时最怕把原始签名弄丢",[1505],{"type":32,"value":1506},"代理模式：包装额外行为时，最怕把原始签名弄丢",{"type":27,"tag":28,"props":1508,"children":1509},{},[1510],{"type":32,"value":1511},"代理模式常用于日志、缓存、权限检查和重试。它最常见的问题是：包装之后，原函数签名丢了，返回值也宽了，调用方只能面对一个模糊函数。",{"type":27,"tag":96,"props":1513,"children":1516},{"className":1514,"code":1515,"language":1409,"meta":7},[1407],"function withTiming\u003CTArgs extends unknown[], TResult>(\n  fn: (...args: TArgs) => Promise\u003CTResult>\n) {\n  return async (...args: TArgs): Promise\u003CTResult> => {\n    const start = performance.now()\n    try {\n      return await fn(...args)\n    } finally {\n      console.log('cost', performance.now() - start)\n    }\n  }\n}\n",[1517],{"type":27,"tag":40,"props":1518,"children":1519},{"__ignoreMap":7},[1520],{"type":32,"value":1515},{"type":27,"tag":28,"props":1522,"children":1523},{},[1524],{"type":32,"value":1525},"这类泛型代理的价值在于：你加了额外行为，但原始参数和返回值仍然保留下来。否则代理模式会把“附加控制”变成“类型信息丢失”。",{"type":27,"tag":49,"props":1527,"children":1529},{"id":1528},"一个常见失败案例模式是抽象了类型却退化成-any",[1530],{"type":32,"value":1531},"一个常见失败案例：模式是抽象了，类型却退化成 any",{"type":27,"tag":28,"props":1533,"children":1534},{},[1535],{"type":32,"value":1536},"很多项目在做所谓“模式升级”时，会出现一种反效果：结构更复杂了，类型却更差了。常见表现有：",{"type":27,"tag":1293,"props":1538,"children":1539},{},[1540,1551,1556,1567],{"type":27,"tag":64,"props":1541,"children":1542},{},[1543,1545],{"type":32,"value":1544},"工厂接收 ",{"type":27,"tag":40,"props":1546,"children":1548},{"className":1547},[],[1549],{"type":32,"value":1550},"config: any",{"type":27,"tag":64,"props":1552,"children":1553},{},[1554],{"type":32,"value":1555},"策略表写成对象，但 key 没有联合类型约束",{"type":27,"tag":64,"props":1557,"children":1558},{},[1559,1561],{"type":32,"value":1560},"适配器只返回 ",{"type":27,"tag":40,"props":1562,"children":1564},{"className":1563},[],[1565],{"type":32,"value":1566},"Record\u003Cstring, unknown>",{"type":27,"tag":64,"props":1568,"children":1569},{},[1570],{"type":32,"value":1571},"代理函数包装后不保留原始签名",{"type":27,"tag":28,"props":1573,"children":1574},{},[1575],{"type":32,"value":1576},"这种代码看起来“更有架构感”，实际上却把很多风险从编译期挪回了运行时。模式如果不能提升边界清晰度和替换安全性，通常只是引入了新的复杂度。",{"type":27,"tag":49,"props":1578,"children":1580},{"id":1579},"决策表什么时候该上模式什么时候先别上",[1581],{"type":32,"value":1582},"决策表：什么时候该上模式，什么时候先别上",{"type":27,"tag":750,"props":1584,"children":1585},{},[1586,1607],{"type":27,"tag":754,"props":1587,"children":1588},{},[1589],{"type":27,"tag":758,"props":1590,"children":1591},{},[1592,1597,1602],{"type":27,"tag":762,"props":1593,"children":1594},{},[1595],{"type":32,"value":1596},"场景",{"type":27,"tag":762,"props":1598,"children":1599},{},[1600],{"type":32,"value":1601},"更适合什么模式",{"type":27,"tag":762,"props":1603,"children":1604},{},[1605],{"type":32,"value":1606},"不建议做的事",{"type":27,"tag":783,"props":1608,"children":1609},{},[1610,1628,1646,1664],{"type":27,"tag":758,"props":1611,"children":1612},{},[1613,1618,1623],{"type":27,"tag":790,"props":1614,"children":1615},{},[1616],{"type":32,"value":1617},"创建逻辑随环境变化",{"type":27,"tag":790,"props":1619,"children":1620},{},[1621],{"type":32,"value":1622},"工厂",{"type":27,"tag":790,"props":1624,"children":1625},{},[1626],{"type":32,"value":1627},"把所有可选项塞进一个大配置对象",{"type":27,"tag":758,"props":1629,"children":1630},{},[1631,1636,1641],{"type":27,"tag":790,"props":1632,"children":1633},{},[1634],{"type":32,"value":1635},"规则可替换、可扩展",{"type":27,"tag":790,"props":1637,"children":1638},{},[1639],{"type":32,"value":1640},"策略",{"type":27,"tag":790,"props":1642,"children":1643},{},[1644],{"type":32,"value":1645},"继续堆 if else 并靠注释解释",{"type":27,"tag":758,"props":1647,"children":1648},{},[1649,1654,1659],{"type":27,"tag":790,"props":1650,"children":1651},{},[1652],{"type":32,"value":1653},"第三方结构不稳定",{"type":27,"tag":790,"props":1655,"children":1656},{},[1657],{"type":32,"value":1658},"适配器",{"type":27,"tag":790,"props":1660,"children":1661},{},[1662],{"type":32,"value":1663},"让业务层到处处理字段差异",{"type":27,"tag":758,"props":1665,"children":1666},{},[1667,1672,1677],{"type":27,"tag":790,"props":1668,"children":1669},{},[1670],{"type":32,"value":1671},"需要附加日志、缓存、权限",{"type":27,"tag":790,"props":1673,"children":1674},{},[1675],{"type":32,"value":1676},"代理",{"type":27,"tag":790,"props":1678,"children":1679},{},[1680],{"type":32,"value":1681},"包装后丢失原函数签名",{"type":27,"tag":49,"props":1683,"children":1684},{"id":834},[1685],{"type":32,"value":834},{"type":27,"tag":28,"props":1687,"children":1688},{},[1689],{"type":32,"value":1690},"TypeScript 里的设计模式，关键不是“更像面向对象”，而是让变化面、稳定面和边界责任表达得更清楚。工厂控制创建，策略控制规则替换，适配器隔离外部差异，代理保留签名的同时叠加附加能力。只要模式引入后类型信息还在，团队就能真正享受到抽象带来的收益。",{"type":27,"tag":28,"props":1692,"children":1693},{},[1694],{"type":32,"value":1695},"本系列导航：",{"type":27,"tag":1293,"props":1697,"children":1698},{},[1699,1710,1721],{"type":27,"tag":64,"props":1700,"children":1701},{},[1702,1704],{"type":32,"value":1703},"如果你想先稳住抽象层对外承诺，接着看 ",{"type":27,"tag":1300,"props":1705,"children":1707},{"href":1706},"/topics/typescript/typescript-public-api-design-exported-types-breaking-change-control",[1708],{"type":32,"value":1709},"TypeScript 公共 API 设计",{"type":27,"tag":64,"props":1711,"children":1712},{},[1713,1715],{"type":32,"value":1714},"若你要把模式继续落到测试和用例上，再看 ",{"type":27,"tag":1300,"props":1716,"children":1718},{"href":1717},"/topics/typescript/typescript-test-data-builders-fixtures-mock-type-safety",[1719],{"type":32,"value":1720},"TypeScript 测试数据构建",{"type":27,"tag":64,"props":1722,"children":1723},{},[1724,1726],{"type":32,"value":1725},"如果你接下来要处理业务状态与错误流，再读 ",{"type":27,"tag":1300,"props":1727,"children":1729},{"href":1728},"/topics/typescript/typescript-form-state-validation-error-modeling-guide",[1730],{"type":32,"value":1731},"TypeScript 表单与错误状态建模",{"type":27,"tag":28,"props":1733,"children":1734},{},[1735],{"type":32,"value":1736},"延伸阅读：",{"type":27,"tag":1293,"props":1738,"children":1739},{},[1740,1749,1757,1764],{"type":27,"tag":64,"props":1741,"children":1742},{},[1743],{"type":27,"tag":1300,"props":1744,"children":1746},{"href":1745},"/topics/typescript/typescript-generic-constraints-conditional-decision-guide",[1747],{"type":32,"value":1748},"TypeScript 泛型约束与条件泛型的实际决策",{"type":27,"tag":64,"props":1750,"children":1751},{},[1752],{"type":27,"tag":1300,"props":1753,"children":1754},{"href":4},[1755],{"type":32,"value":1756},"TypeScript 可辨识联合与穷举检查",{"type":27,"tag":64,"props":1758,"children":1759},{},[1760],{"type":27,"tag":1300,"props":1761,"children":1762},{"href":1706},[1763],{"type":32,"value":1709},{"type":27,"tag":64,"props":1765,"children":1766},{},[1767],{"type":27,"tag":1300,"props":1768,"children":1770},{"href":1769},"/topics/frontend/vue-component-design-patterns-essentials",[1771],{"type":32,"value":1772},"Vue 组件设计模式精选",{"title":7,"searchDepth":901,"depth":901,"links":1774},[1775,1776,1777,1778,1779,1780,1781],{"id":1395,"depth":904,"text":1398},{"id":1430,"depth":904,"text":1433},{"id":1462,"depth":904,"text":1465},{"id":1503,"depth":904,"text":1506},{"id":1528,"depth":904,"text":1531},{"id":1579,"depth":904,"text":1582},{"id":834,"depth":904,"text":834},"content:topics:typescript:typescript-design-patterns-factory-strategy-adapter-proxy.md","topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy.md","topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy",{"_path":1786,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1787,"description":1788,"date":1362,"topic":5,"author":11,"tags":1789,"image":1794,"imageQuery":1795,"pexelsPhotoId":1796,"pexelsUrl":1797,"featured":6,"readingTime":1798,"body":1799,"_type":913,"_id":2318,"_source":915,"_file":2319,"_stem":2320,"_extension":918},"/topics/typescript/typescript-event-map-payload-contract-subscription-safety","TypeScript 事件系统建模：事件映射、payload 约束与订阅端类型安全","事件系统最容易失控的地方，不是发不出去，而是名字、payload 和订阅约定慢慢漂移。本文从 event map、发布订阅 API 设计和版本演进出发，讲清 TypeScript 如何让事件系统保持可协作。",[13,1790,1791,1792,1793],"Event Map","PubSub","Payload","Event Driven","/images/articles/typescript-event-map-payload-contract-subscription-safety-featured.jpg","event driven architecture code laptop",10826689,"https://www.pexels.com/photo/a-laptop-on-the-table-10826689/",16,{"type":24,"children":1800,"toc":2308},[1801,1838,1843,1849,1858,1863,1876,1881,1887,1892,1901,1906,1915,1920,1926,1931,1944,1956,1961,1979,1985,1990,1995,2013,2018,2024,2044,2062,2067,2073,2078,2101,2106,2111,2139,2143,2148,2153,2235,2239,2269,2273],{"type":27,"tag":28,"props":1802,"children":1803},{},[1804,1806,1812,1814,1820,1822,1828,1830,1836],{"type":32,"value":1805},"很多系统一开始的事件总线都很轻：",{"type":27,"tag":40,"props":1807,"children":1809},{"className":1808},[],[1810],{"type":32,"value":1811},"emit(name, payload)",{"type":32,"value":1813},"，再配一个 ",{"type":27,"tag":40,"props":1815,"children":1817},{"className":1816},[],[1818],{"type":32,"value":1819},"on(name, handler)",{"type":32,"value":1821}," 就能跑起来。真正的问题不会立刻出现，而是随着事件增多、订阅方变多、场景变复杂，名字和 payload 会慢慢失去同步。某个地方仍然发 ",{"type":27,"tag":40,"props":1823,"children":1825},{"className":1824},[],[1826],{"type":32,"value":1827},"user.updated",{"type":32,"value":1829},"，另一个地方已经开始监听 ",{"type":27,"tag":40,"props":1831,"children":1833},{"className":1832},[],[1834],{"type":32,"value":1835},"user.profile.updated",{"type":32,"value":1837},"；某次迭代里 payload 多了一个字段，但下游还按旧形态读取；有的 handler 以为收到的是单对象，另一个地方却开始传数组。",{"type":27,"tag":28,"props":1839,"children":1840},{},[1841],{"type":32,"value":1842},"TypeScript 在事件系统里的价值，不是把事件总线写得更花，而是把“事件名和 payload 的对应关系”变成可检查的契约。只要这一层没有被显式建模，系统后面再怎么拆模块、加队列、做回放，事件协作都很容易继续靠记忆和文档维持。",{"type":27,"tag":49,"props":1844,"children":1846},{"id":1845},"最危险的接口长这样事件名是-stringpayload-是-any",[1847],{"type":32,"value":1848},"最危险的接口长这样：事件名是 string，payload 是 any",{"type":27,"tag":96,"props":1850,"children":1853},{"className":1851,"code":1852,"language":1409,"meta":7},[1407],"function emit(event: string, payload: any) {\n  // ...\n}\n",[1854],{"type":27,"tag":40,"props":1855,"children":1856},{"__ignoreMap":7},[1857],{"type":32,"value":1852},{"type":27,"tag":28,"props":1859,"children":1860},{},[1861],{"type":32,"value":1862},"这类写法的坏处不是“没有类型提示”这么简单，而是它让系统的两个核心约束完全脱离了关系：",{"type":27,"tag":1293,"props":1864,"children":1865},{},[1866,1871],{"type":27,"tag":64,"props":1867,"children":1868},{},[1869],{"type":32,"value":1870},"哪些事件名是合法的。",{"type":27,"tag":64,"props":1872,"children":1873},{},[1874],{"type":32,"value":1875},"某个事件对应的 payload 应该长什么样。",{"type":27,"tag":28,"props":1877,"children":1878},{},[1879],{"type":32,"value":1880},"只要这两个约束还靠人脑维护，规模一大就一定会漂移。",{"type":27,"tag":49,"props":1882,"children":1884},{"id":1883},"event-map-是最稳的起点先把名字和-payload-绑定起来",[1885],{"type":32,"value":1886},"event map 是最稳的起点：先把名字和 payload 绑定起来",{"type":27,"tag":28,"props":1888,"children":1889},{},[1890],{"type":32,"value":1891},"一种非常实用的建模方式，是先定义事件映射表：",{"type":27,"tag":96,"props":1893,"children":1896},{"className":1894,"code":1895,"language":1409,"meta":7},[1407],"type AppEventMap = {\n  'user.created': { id: string; source: 'admin' | 'self-service' }\n  'user.deleted': { id: string; reason?: string }\n  'invoice.paid': { invoiceId: string; amount: number }\n}\n",[1897],{"type":27,"tag":40,"props":1898,"children":1899},{"__ignoreMap":7},[1900],{"type":32,"value":1895},{"type":27,"tag":28,"props":1902,"children":1903},{},[1904],{"type":32,"value":1905},"一旦这张表存在，发布和订阅接口都可以围绕它推导：",{"type":27,"tag":96,"props":1907,"children":1910},{"className":1908,"code":1909,"language":1409,"meta":7},[1407],"function emit\u003CK extends keyof AppEventMap>(\n  event: K,\n  payload: AppEventMap[K]\n) {}\n\nfunction on\u003CK extends keyof AppEventMap>(\n  event: K,\n  handler: (payload: AppEventMap[K]) => void\n) {}\n",[1911],{"type":27,"tag":40,"props":1912,"children":1913},{"__ignoreMap":7},[1914],{"type":32,"value":1909},{"type":27,"tag":28,"props":1916,"children":1917},{},[1918],{"type":32,"value":1919},"这类 API 的意义不只是补全更舒服，而是让“事件名”和“事件负载”在类型层面无法脱钩。只要事件名选错、payload 形状不对，问题会在提交代码前就暴露。",{"type":27,"tag":49,"props":1921,"children":1923},{"id":1922},"事件设计真正要防的是同名异义和异名同义",[1924],{"type":32,"value":1925},"事件设计真正要防的是“同名异义”和“异名同义”",{"type":27,"tag":28,"props":1927,"children":1928},{},[1929],{"type":32,"value":1930},"事件系统最容易出现的两类混乱是：",{"type":27,"tag":1293,"props":1932,"children":1933},{},[1934,1939],{"type":27,"tag":64,"props":1935,"children":1936},{},[1937],{"type":32,"value":1938},"同名异义：同一个事件名在不同模块里承载不同语义。",{"type":27,"tag":64,"props":1940,"children":1941},{},[1942],{"type":32,"value":1943},"异名同义：同一类业务事实被多个名字重复表达。",{"type":27,"tag":28,"props":1945,"children":1946},{},[1947,1949,1954],{"type":32,"value":1948},"比如 ",{"type":27,"tag":40,"props":1950,"children":1952},{"className":1951},[],[1953],{"type":32,"value":1827},{"type":32,"value":1955}," 这个名字，看起来什么都能装。用户改昵称、改角色、改手机号、改订阅偏好都能叫 updated。短期很方便，长期却让订阅方不知道自己到底该监听什么，也让 payload 越长越杂。",{"type":27,"tag":28,"props":1957,"children":1958},{},[1959],{"type":32,"value":1960},"更稳的做法往往是：",{"type":27,"tag":1293,"props":1962,"children":1963},{},[1964,1969,1974],{"type":27,"tag":64,"props":1965,"children":1966},{},[1967],{"type":32,"value":1968},"名称按业务事实切分，而不是按“发生过变化”这种宽泛概念命名。",{"type":27,"tag":64,"props":1970,"children":1971},{},[1972],{"type":32,"value":1973},"payload 只携带当前事件需要承诺的最小信息。",{"type":27,"tag":64,"props":1975,"children":1976},{},[1977],{"type":32,"value":1978},"大量共享字段通过公共对象或 metadata 包装，而不是每个事件都随意展开。",{"type":27,"tag":49,"props":1980,"children":1982},{"id":1981},"订阅端类型安全的关键不在泛型而在-handler-语义是否稳定",[1983],{"type":32,"value":1984},"订阅端类型安全的关键，不在泛型，而在 handler 语义是否稳定",{"type":27,"tag":28,"props":1986,"children":1987},{},[1988],{"type":32,"value":1989},"发布端和订阅端的类型常常被讨论成“泛型能不能写出来”，其实更重要的问题是：handler 是否能基于事件名做出稳定假设。如果一个事件 payload 经常改、字段意义经常变，哪怕泛型写对了，订阅端也不会真正稳定。",{"type":27,"tag":28,"props":1991,"children":1992},{},[1993],{"type":32,"value":1994},"所以团队在设计 event map 时，最好把这几件事一起定清：",{"type":27,"tag":1293,"props":1996,"children":1997},{},[1998,2003,2008],{"type":27,"tag":64,"props":1999,"children":2000},{},[2001],{"type":32,"value":2002},"这个事件代表什么业务事实。",{"type":27,"tag":64,"props":2004,"children":2005},{},[2006],{"type":32,"value":2007},"订阅方最少需要拿到哪些字段。",{"type":27,"tag":64,"props":2009,"children":2010},{},[2011],{"type":32,"value":2012},"哪些字段未来允许扩展，哪些字段一旦变化就算破坏式变更。",{"type":27,"tag":28,"props":2014,"children":2015},{},[2016],{"type":32,"value":2017},"TypeScript 能帮你守住结构，但不能替你定义清业务语义。",{"type":27,"tag":49,"props":2019,"children":2021},{"id":2020},"一个常见失败案例事件总线很通用但所有变更都在悄悄破约",[2022],{"type":32,"value":2023},"一个常见失败案例：事件总线很“通用”，但所有变更都在悄悄破约",{"type":27,"tag":28,"props":2025,"children":2026},{},[2027,2029,2035,2036,2042],{"type":32,"value":2028},"某团队有一套抽象得很漂亮的事件总线封装，",{"type":27,"tag":40,"props":2030,"children":2032},{"className":2031},[],[2033],{"type":32,"value":2034},"emit",{"type":32,"value":165},{"type":27,"tag":40,"props":2037,"children":2039},{"className":2038},[],[2040],{"type":32,"value":2041},"on",{"type":32,"value":2043}," 都做成了泛型，但事件名本身没有中心化建模，而是由各模块自己声明字符串字面量。结果几个月后出现了典型问题：",{"type":27,"tag":1293,"props":2045,"children":2046},{},[2047,2052,2057],{"type":27,"tag":64,"props":2048,"children":2049},{},[2050],{"type":32,"value":2051},"同一个名字在多个地方被重复定义。",{"type":27,"tag":64,"props":2053,"children":2054},{},[2055],{"type":32,"value":2056},"payload 字段逐步追加，但没有人通知所有订阅者。",{"type":27,"tag":64,"props":2058,"children":2059},{},[2060],{"type":32,"value":2061},"少数 handler 开始自己断言 payload 类型，绕过总线约束。",{"type":27,"tag":28,"props":2063,"children":2064},{},[2065],{"type":32,"value":2066},"问题不在总线 API，而在契约没有集中。只要 event map 不是系统级事实，而是多个文件分散声明，漂移迟早会发生。",{"type":27,"tag":49,"props":2068,"children":2070},{"id":2069},"事件版本演进要像-api-一样认真",[2071],{"type":32,"value":2072},"事件版本演进要像 API 一样认真",{"type":27,"tag":28,"props":2074,"children":2075},{},[2076],{"type":32,"value":2077},"很多团队对 HTTP API 很谨慎，却对事件变更很随意。实际上，事件一旦被多个消费者订阅，它就是另一种 API。比较值得固定的规则包括：",{"type":27,"tag":1293,"props":2079,"children":2080},{},[2081,2086,2091,2096],{"type":27,"tag":64,"props":2082,"children":2083},{},[2084],{"type":32,"value":2085},"payload 新增字段通常问题不大，但删除和重命名要视作破坏式变更。",{"type":27,"tag":64,"props":2087,"children":2088},{},[2089],{"type":32,"value":2090},"需要重大语义变化时，优先引入新事件名，而不是偷偷改旧 payload。",{"type":27,"tag":64,"props":2092,"children":2093},{},[2094],{"type":32,"value":2095},"公共事件和内部事件分层，不要让局部实现细节暴露给全局订阅方。",{"type":27,"tag":64,"props":2097,"children":2098},{},[2099],{"type":32,"value":2100},"如果事件跨进程或跨服务传播，运行时 schema 校验也要补上。",{"type":27,"tag":28,"props":2102,"children":2103},{},[2104],{"type":32,"value":2105},"事件不是“消息发出去就算完”，而是长期协作契约的一部分。",{"type":27,"tag":49,"props":2107,"children":2109},{"id":2108},"一份事件系统建模检查表",[2110],{"type":32,"value":2108},{"type":27,"tag":1293,"props":2112,"children":2113},{},[2114,2119,2124,2129,2134],{"type":27,"tag":64,"props":2115,"children":2116},{},[2117],{"type":32,"value":2118},"事件名和 payload 是否被一张中心化 event map 绑定。",{"type":27,"tag":64,"props":2120,"children":2121},{},[2122],{"type":32,"value":2123},"事件命名是否表达业务事实，而不是宽泛动作。",{"type":27,"tag":64,"props":2125,"children":2126},{},[2127],{"type":32,"value":2128},"payload 是否只承诺最小必要字段，而非把整个对象一股脑透出去。",{"type":27,"tag":64,"props":2130,"children":2131},{},[2132],{"type":32,"value":2133},"订阅方是否能仅凭事件名获得稳定的 payload 类型。",{"type":27,"tag":64,"props":2135,"children":2136},{},[2137],{"type":32,"value":2138},"破坏式变更是否按 API 升级一样被认真对待。",{"type":27,"tag":49,"props":2140,"children":2141},{"id":834},[2142],{"type":32,"value":834},{"type":27,"tag":28,"props":2144,"children":2145},{},[2146],{"type":32,"value":2147},"TypeScript 让事件系统最值得做的一件事，就是把“事件名和 payload 的关系”从口头约定变成编译期契约。只要 event map 足够集中、命名足够克制、版本演进足够明确，发布订阅就不会随着规模扩大而变成字符串驱动的隐性耦合。",{"type":27,"tag":28,"props":2149,"children":2150},{},[2151],{"type":32,"value":2152},"本批次专题导航：",{"type":27,"tag":1293,"props":2154,"children":2155},{},[2156,2182,2205,2220],{"type":27,"tag":64,"props":2157,"children":2158},{},[2159,2161,2167,2169,2175,2176],{"type":32,"value":2160},"工程边界：",{"type":27,"tag":1300,"props":2162,"children":2164},{"href":2163},"/topics/typescript/typescript-project-references-tsconfig-layering-incremental-build-boundaries",[2165],{"type":32,"value":2166},"TypeScript 项目引用与 tsconfig 分层",{"type":32,"value":2168},"、",{"type":27,"tag":1300,"props":2170,"children":2172},{"href":2171},"/topics/typescript/typescript-monorepo-dependency-boundaries-path-alias-exports-cycles",[2173],{"type":32,"value":2174},"TypeScript Monorepo 依赖边界治理",{"type":32,"value":2168},{"type":27,"tag":1300,"props":2177,"children":2179},{"href":2178},"/topics/typescript/typescript-typecheck-performance-optimization-large-repo-playbook",[2180],{"type":32,"value":2181},"TypeScript 类型检查性能优化",{"type":27,"tag":64,"props":2183,"children":2184},{},[2185,2187,2191,2192,2198,2199],{"type":32,"value":2186},"协议协作：",{"type":27,"tag":1300,"props":2188,"children":2189},{"href":1706},[2190],{"type":32,"value":1709},{"type":32,"value":2168},{"type":27,"tag":1300,"props":2193,"children":2195},{"href":2194},"/topics/typescript/typescript-runtime-validation-static-types-schema-error-modeling",[2196],{"type":32,"value":2197},"TypeScript 运行时校验与静态类型协作",{"type":32,"value":2168},{"type":27,"tag":1300,"props":2200,"children":2202},{"href":2201},"/topics/typescript/typescript-openapi-contract-codegen-handwritten-types-versioning",[2203],{"type":32,"value":2204},"TypeScript 与 OpenAPI 契约协同",{"type":27,"tag":64,"props":2206,"children":2207},{},[2208,2210,2215,2216],{"type":32,"value":2209},"落地复用：",{"type":27,"tag":1300,"props":2211,"children":2212},{"href":1359},[2213],{"type":32,"value":2214},"TypeScript 设计模式实战",{"type":32,"value":2168},{"type":27,"tag":1300,"props":2217,"children":2218},{"href":1717},[2219],{"type":32,"value":1720},{"type":27,"tag":64,"props":2221,"children":2222},{},[2223,2225,2230,2231],{"type":32,"value":2224},"状态建模：",{"type":27,"tag":1300,"props":2226,"children":2227},{"href":1786},[2228],{"type":32,"value":2229},"TypeScript 事件系统建模",{"type":32,"value":2168},{"type":27,"tag":1300,"props":2232,"children":2233},{"href":1728},[2234],{"type":32,"value":1731},{"type":27,"tag":28,"props":2236,"children":2237},{},[2238],{"type":32,"value":1695},{"type":27,"tag":1293,"props":2240,"children":2241},{},[2242,2251,2260],{"type":27,"tag":64,"props":2243,"children":2244},{},[2245,2247],{"type":32,"value":2246},"如果你还没把边界输入做成可信数据，先读 ",{"type":27,"tag":1300,"props":2248,"children":2249},{"href":2194},[2250],{"type":32,"value":2197},{"type":27,"tag":64,"props":2252,"children":2253},{},[2254,2256],{"type":32,"value":2255},"若你要把事件流继续落到页面状态，再看 ",{"type":27,"tag":1300,"props":2257,"children":2258},{"href":1728},[2259],{"type":32,"value":1731},{"type":27,"tag":64,"props":2261,"children":2262},{},[2263,2265],{"type":32,"value":2264},"如果你希望对外契约和事件契约一起收口，可读 ",{"type":27,"tag":1300,"props":2266,"children":2267},{"href":1706},[2268],{"type":32,"value":1709},{"type":27,"tag":28,"props":2270,"children":2271},{},[2272],{"type":32,"value":1736},{"type":27,"tag":1293,"props":2274,"children":2275},{},[2276,2285,2292,2299],{"type":27,"tag":64,"props":2277,"children":2278},{},[2279],{"type":27,"tag":1300,"props":2280,"children":2282},{"href":2281},"/topics/typescript/typescript-template-literal-types-real-world-applications",[2283],{"type":32,"value":2284},"TypeScript 模板字面量类型在真实项目中的应用",{"type":27,"tag":64,"props":2286,"children":2287},{},[2288],{"type":27,"tag":1300,"props":2289,"children":2290},{"href":4},[2291],{"type":32,"value":1756},{"type":27,"tag":64,"props":2293,"children":2294},{},[2295],{"type":27,"tag":1300,"props":2296,"children":2297},{"href":2194},[2298],{"type":32,"value":2197},{"type":27,"tag":64,"props":2300,"children":2301},{},[2302],{"type":27,"tag":1300,"props":2303,"children":2305},{"href":2304},"/topics/realtime-applications-guide",[2306],{"type":32,"value":2307},"实时应用开发完全指南",{"title":7,"searchDepth":901,"depth":901,"links":2309},[2310,2311,2312,2313,2314,2315,2316,2317],{"id":1845,"depth":904,"text":1848},{"id":1883,"depth":904,"text":1886},{"id":1922,"depth":904,"text":1925},{"id":1981,"depth":904,"text":1984},{"id":2020,"depth":904,"text":2023},{"id":2069,"depth":904,"text":2072},{"id":2108,"depth":904,"text":2108},{"id":834,"depth":904,"text":834},"content:topics:typescript:typescript-event-map-payload-contract-subscription-safety.md","topics/typescript/typescript-event-map-payload-contract-subscription-safety.md","topics/typescript/typescript-event-map-payload-contract-subscription-safety",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":2322,"image":18,"imageQuery":19,"pexelsPhotoId":20,"pexelsUrl":21,"featured":6,"readingTime":22,"body":2323,"_type":913,"_id":914,"_source":915,"_file":916,"_stem":917,"_extension":918},[13,14,15,16,17],{"type":24,"children":2324,"toc":3023},[2325,2329,2339,2343,2347,2374,2382,2434,2438,2454,2462,2478,2494,2498,2514,2522,2574,2608,2612,2616,2624,2640,2648,2652,2656,2664,2704,2708,2712,2720,2772,2776,2780,2788,2816,2820,2824,2832,2896,2900,2966,2970,2974,2978],{"type":27,"tag":28,"props":2326,"children":2327},{},[2328],{"type":32,"value":33},{"type":27,"tag":28,"props":2330,"children":2331},{},[2332,2333,2338],{"type":32,"value":38},{"type":27,"tag":40,"props":2334,"children":2336},{"className":2335},[],[2337],{"type":32,"value":45},{"type":32,"value":47},{"type":27,"tag":49,"props":2340,"children":2341},{"id":51},[2342],{"type":32,"value":51},{"type":27,"tag":28,"props":2344,"children":2345},{},[2346],{"type":32,"value":58},{"type":27,"tag":60,"props":2348,"children":2349},{},[2350,2354,2358],{"type":27,"tag":64,"props":2351,"children":2352},{},[2353],{"type":32,"value":68},{"type":27,"tag":64,"props":2355,"children":2356},{},[2357],{"type":32,"value":73},{"type":27,"tag":64,"props":2359,"children":2360},{},[2361,2362,2367,2368,2373],{"type":32,"value":78},{"type":27,"tag":40,"props":2363,"children":2365},{"className":2364},[],[2366],{"type":32,"value":84},{"type":32,"value":86},{"type":27,"tag":40,"props":2369,"children":2371},{"className":2370},[],[2372],{"type":32,"value":92},{"type":32,"value":94},{"type":27,"tag":96,"props":2375,"children":2377},{"className":2376,"code":100,"language":5,"meta":7},[99],[2378],{"type":27,"tag":40,"props":2379,"children":2380},{"__ignoreMap":7},[2381],{"type":32,"value":100},{"type":27,"tag":28,"props":2383,"children":2384},{},[2385,2386,2391,2392,2397,2398,2403,2404,2409,2410,2415,2416,2421,2422,2427,2428,2433],{"type":32,"value":110},{"type":27,"tag":40,"props":2387,"children":2389},{"className":2388},[],[2390],{"type":32,"value":116},{"type":32,"value":118},{"type":27,"tag":40,"props":2393,"children":2395},{"className":2394},[],[2396],{"type":32,"value":124},{"type":32,"value":126},{"type":27,"tag":40,"props":2399,"children":2401},{"className":2400},[],[2402],{"type":32,"value":132},{"type":32,"value":134},{"type":27,"tag":40,"props":2405,"children":2407},{"className":2406},[],[2408],{"type":32,"value":140},{"type":32,"value":142},{"type":27,"tag":40,"props":2411,"children":2413},{"className":2412},[],[2414],{"type":32,"value":148},{"type":32,"value":150},{"type":27,"tag":40,"props":2417,"children":2419},{"className":2418},[],[2420],{"type":32,"value":156},{"type":32,"value":158},{"type":27,"tag":40,"props":2423,"children":2425},{"className":2424},[],[2426],{"type":32,"value":124},{"type":32,"value":165},{"type":27,"tag":40,"props":2429,"children":2431},{"className":2430},[],[2432],{"type":32,"value":140},{"type":32,"value":172},{"type":27,"tag":49,"props":2435,"children":2436},{"id":175},[2437],{"type":32,"value":175},{"type":27,"tag":28,"props":2439,"children":2440},{},[2441,2442,2447,2448,2453],{"type":32,"value":182},{"type":27,"tag":40,"props":2443,"children":2445},{"className":2444},[],[2446],{"type":32,"value":45},{"type":32,"value":189},{"type":27,"tag":40,"props":2449,"children":2451},{"className":2450},[],[2452],{"type":32,"value":195},{"type":32,"value":197},{"type":27,"tag":96,"props":2455,"children":2457},{"className":2456,"code":201,"language":5,"meta":7},[99],[2458],{"type":27,"tag":40,"props":2459,"children":2460},{"__ignoreMap":7},[2461],{"type":32,"value":201},{"type":27,"tag":28,"props":2463,"children":2464},{},[2465,2466,2471,2472,2477],{"type":32,"value":211},{"type":27,"tag":40,"props":2467,"children":2469},{"className":2468},[],[2470],{"type":32,"value":195},{"type":32,"value":218},{"type":27,"tag":40,"props":2473,"children":2475},{"className":2474},[],[2476],{"type":32,"value":224},{"type":32,"value":226},{"type":27,"tag":28,"props":2479,"children":2480},{},[2481,2482,2487,2488,2493],{"type":32,"value":231},{"type":27,"tag":40,"props":2483,"children":2485},{"className":2484},[],[2486],{"type":32,"value":237},{"type":32,"value":239},{"type":27,"tag":40,"props":2489,"children":2491},{"className":2490},[],[2492],{"type":32,"value":195},{"type":32,"value":246},{"type":27,"tag":49,"props":2495,"children":2496},{"id":249},[2497],{"type":32,"value":249},{"type":27,"tag":28,"props":2499,"children":2500},{},[2501,2502,2507,2508,2513],{"type":32,"value":256},{"type":27,"tag":40,"props":2503,"children":2505},{"className":2504},[],[2506],{"type":32,"value":262},{"type":32,"value":264},{"type":27,"tag":40,"props":2509,"children":2511},{"className":2510},[],[2512],{"type":32,"value":270},{"type":32,"value":272},{"type":27,"tag":96,"props":2515,"children":2517},{"className":2516,"code":276,"language":5,"meta":7},[99],[2518],{"type":27,"tag":40,"props":2519,"children":2520},{"__ignoreMap":7},[2521],{"type":32,"value":276},{"type":27,"tag":28,"props":2523,"children":2524},{},[2525,2526,2531,2532,2537,2538,2543,2544,2549,2550,2555,2556,2561,2562,2567,2568,2573],{"type":32,"value":286},{"type":27,"tag":40,"props":2527,"children":2529},{"className":2528},[],[2530],{"type":32,"value":292},{"type":32,"value":294},{"type":27,"tag":40,"props":2533,"children":2535},{"className":2534},[],[2536],{"type":32,"value":300},{"type":32,"value":302},{"type":27,"tag":40,"props":2539,"children":2541},{"className":2540},[],[2542],{"type":32,"value":195},{"type":32,"value":309},{"type":27,"tag":40,"props":2545,"children":2547},{"className":2546},[],[2548],{"type":32,"value":315},{"type":32,"value":317},{"type":27,"tag":40,"props":2551,"children":2553},{"className":2552},[],[2554],{"type":32,"value":323},{"type":32,"value":325},{"type":27,"tag":40,"props":2557,"children":2559},{"className":2558},[],[2560],{"type":32,"value":331},{"type":32,"value":333},{"type":27,"tag":40,"props":2563,"children":2565},{"className":2564},[],[2566],{"type":32,"value":300},{"type":32,"value":340},{"type":27,"tag":40,"props":2569,"children":2571},{"className":2570},[],[2572],{"type":32,"value":270},{"type":32,"value":347},{"type":27,"tag":28,"props":2575,"children":2576},{},[2577,2578,2583,2584,2589,2590,2595,2596,2601,2602,2607],{"type":32,"value":352},{"type":27,"tag":40,"props":2579,"children":2581},{"className":2580},[],[2582],{"type":32,"value":270},{"type":32,"value":359},{"type":27,"tag":40,"props":2585,"children":2587},{"className":2586},[],[2588],{"type":32,"value":270},{"type":32,"value":366},{"type":27,"tag":40,"props":2591,"children":2593},{"className":2592},[],[2594],{"type":32,"value":270},{"type":32,"value":373},{"type":27,"tag":40,"props":2597,"children":2599},{"className":2598},[],[2600],{"type":32,"value":270},{"type":32,"value":380},{"type":27,"tag":40,"props":2603,"children":2605},{"className":2604},[],[2606],{"type":32,"value":270},{"type":32,"value":387},{"type":27,"tag":49,"props":2609,"children":2610},{"id":390},[2611],{"type":32,"value":393},{"type":27,"tag":28,"props":2613,"children":2614},{},[2615],{"type":32,"value":398},{"type":27,"tag":96,"props":2617,"children":2619},{"className":2618,"code":402,"language":5,"meta":7},[99],[2620],{"type":27,"tag":40,"props":2621,"children":2622},{"__ignoreMap":7},[2623],{"type":32,"value":402},{"type":27,"tag":28,"props":2625,"children":2626},{},[2627,2628,2633,2634,2639],{"type":32,"value":412},{"type":27,"tag":40,"props":2629,"children":2631},{"className":2630},[],[2632],{"type":32,"value":84},{"type":32,"value":419},{"type":27,"tag":40,"props":2635,"children":2637},{"className":2636},[],[2638],{"type":32,"value":425},{"type":32,"value":427},{"type":27,"tag":96,"props":2641,"children":2643},{"className":2642,"code":431,"language":5,"meta":7},[99],[2644],{"type":27,"tag":40,"props":2645,"children":2646},{"__ignoreMap":7},[2647],{"type":32,"value":431},{"type":27,"tag":49,"props":2649,"children":2650},{"id":439},[2651],{"type":32,"value":442},{"type":27,"tag":28,"props":2653,"children":2654},{},[2655],{"type":32,"value":447},{"type":27,"tag":96,"props":2657,"children":2659},{"className":2658,"code":451,"language":5,"meta":7},[99],[2660],{"type":27,"tag":40,"props":2661,"children":2662},{"__ignoreMap":7},[2663],{"type":32,"value":451},{"type":27,"tag":28,"props":2665,"children":2666},{},[2667,2668,2673,2674,2679,2680,2685,2686,2691,2692,2697,2698,2703],{"type":32,"value":461},{"type":27,"tag":40,"props":2669,"children":2671},{"className":2670},[],[2672],{"type":32,"value":467},{"type":32,"value":165},{"type":27,"tag":40,"props":2675,"children":2677},{"className":2676},[],[2678],{"type":32,"value":474},{"type":32,"value":476},{"type":27,"tag":40,"props":2681,"children":2683},{"className":2682},[],[2684],{"type":32,"value":482},{"type":32,"value":484},{"type":27,"tag":40,"props":2687,"children":2689},{"className":2688},[],[2690],{"type":32,"value":490},{"type":32,"value":492},{"type":27,"tag":40,"props":2693,"children":2695},{"className":2694},[],[2696],{"type":32,"value":498},{"type":32,"value":500},{"type":27,"tag":40,"props":2699,"children":2701},{"className":2700},[],[2702],{"type":32,"value":506},{"type":32,"value":508},{"type":27,"tag":49,"props":2705,"children":2706},{"id":511},[2707],{"type":32,"value":514},{"type":27,"tag":28,"props":2709,"children":2710},{},[2711],{"type":32,"value":519},{"type":27,"tag":96,"props":2713,"children":2715},{"className":2714,"code":523,"language":5,"meta":7},[99],[2716],{"type":27,"tag":40,"props":2717,"children":2718},{"__ignoreMap":7},[2719],{"type":32,"value":523},{"type":27,"tag":28,"props":2721,"children":2722},{},[2723,2724,2729,2730,2735,2736,2741,2742,2747,2748,2753,2754,2759,2760,2765,2766,2771],{"type":32,"value":533},{"type":27,"tag":40,"props":2725,"children":2727},{"className":2726},[],[2728],{"type":32,"value":539},{"type":32,"value":541},{"type":27,"tag":40,"props":2731,"children":2733},{"className":2732},[],[2734],{"type":32,"value":547},{"type":32,"value":549},{"type":27,"tag":40,"props":2737,"children":2739},{"className":2738},[],[2740],{"type":32,"value":555},{"type":32,"value":541},{"type":27,"tag":40,"props":2743,"children":2745},{"className":2744},[],[2746],{"type":32,"value":562},{"type":32,"value":564},{"type":27,"tag":40,"props":2749,"children":2751},{"className":2750},[],[2752],{"type":32,"value":570},{"type":32,"value":549},{"type":27,"tag":40,"props":2755,"children":2757},{"className":2756},[],[2758],{"type":32,"value":577},{"type":32,"value":541},{"type":27,"tag":40,"props":2761,"children":2763},{"className":2762},[],[2764],{"type":32,"value":584},{"type":32,"value":586},{"type":27,"tag":40,"props":2767,"children":2769},{"className":2768},[],[2770],{"type":32,"value":592},{"type":32,"value":594},{"type":27,"tag":49,"props":2773,"children":2774},{"id":597},[2775],{"type":32,"value":597},{"type":27,"tag":28,"props":2777,"children":2778},{},[2779],{"type":32,"value":604},{"type":27,"tag":96,"props":2781,"children":2783},{"className":2782,"code":608,"language":5,"meta":7},[99],[2784],{"type":27,"tag":40,"props":2785,"children":2786},{"__ignoreMap":7},[2787],{"type":32,"value":608},{"type":27,"tag":28,"props":2789,"children":2790},{},[2791,2792,2797,2798,2803,2804,2809,2810,2815],{"type":32,"value":618},{"type":27,"tag":40,"props":2793,"children":2795},{"className":2794},[],[2796],{"type":32,"value":624},{"type":32,"value":626},{"type":27,"tag":40,"props":2799,"children":2801},{"className":2800},[],[2802],{"type":32,"value":632},{"type":32,"value":634},{"type":27,"tag":40,"props":2805,"children":2807},{"className":2806},[],[2808],{"type":32,"value":640},{"type":32,"value":642},{"type":27,"tag":40,"props":2811,"children":2813},{"className":2812},[],[2814],{"type":32,"value":632},{"type":32,"value":649},{"type":27,"tag":49,"props":2817,"children":2818},{"id":652},[2819],{"type":32,"value":655},{"type":27,"tag":28,"props":2821,"children":2822},{},[2823],{"type":32,"value":660},{"type":27,"tag":96,"props":2825,"children":2827},{"className":2826,"code":664,"language":5,"meta":7},[99],[2828],{"type":27,"tag":40,"props":2829,"children":2830},{"__ignoreMap":7},[2831],{"type":32,"value":664},{"type":27,"tag":28,"props":2833,"children":2834},{},[2835,2836,2841,2842,2847,2848,2853,2854,2859,2860,2865,2866,2871,2872,2877,2878,2883,2884,2889,2890,2895],{"type":32,"value":674},{"type":27,"tag":40,"props":2837,"children":2839},{"className":2838},[],[2840],{"type":32,"value":124},{"type":32,"value":165},{"type":27,"tag":40,"props":2843,"children":2845},{"className":2844},[],[2846],{"type":32,"value":140},{"type":32,"value":687},{"type":27,"tag":40,"props":2849,"children":2851},{"className":2850},[],[2852],{"type":32,"value":84},{"type":32,"value":165},{"type":27,"tag":40,"props":2855,"children":2857},{"className":2856},[],[2858],{"type":32,"value":156},{"type":32,"value":700},{"type":27,"tag":40,"props":2861,"children":2863},{"className":2862},[],[2864],{"type":32,"value":706},{"type":32,"value":708},{"type":27,"tag":40,"props":2867,"children":2869},{"className":2868},[],[2870],{"type":32,"value":84},{"type":32,"value":715},{"type":27,"tag":40,"props":2873,"children":2875},{"className":2874},[],[2876],{"type":32,"value":124},{"type":32,"value":722},{"type":27,"tag":40,"props":2879,"children":2881},{"className":2880},[],[2882],{"type":32,"value":706},{"type":32,"value":729},{"type":27,"tag":40,"props":2885,"children":2887},{"className":2886},[],[2888],{"type":32,"value":116},{"type":32,"value":736},{"type":27,"tag":40,"props":2891,"children":2893},{"className":2892},[],[2894],{"type":32,"value":124},{"type":32,"value":743},{"type":27,"tag":28,"props":2897,"children":2898},{},[2899],{"type":32,"value":748},{"type":27,"tag":750,"props":2901,"children":2902},{},[2903,2925],{"type":27,"tag":754,"props":2904,"children":2905},{},[2906],{"type":27,"tag":758,"props":2907,"children":2908},{},[2909,2913,2917,2921],{"type":27,"tag":762,"props":2910,"children":2911},{},[2912],{"type":32,"value":766},{"type":27,"tag":762,"props":2914,"children":2915},{},[2916],{"type":32,"value":771},{"type":27,"tag":762,"props":2918,"children":2919},{},[2920],{"type":32,"value":776},{"type":27,"tag":762,"props":2922,"children":2923},{},[2924],{"type":32,"value":781},{"type":27,"tag":783,"props":2926,"children":2927},{},[2928,2947],{"type":27,"tag":758,"props":2929,"children":2930},{},[2931,2935,2939,2943],{"type":27,"tag":790,"props":2932,"children":2933},{},[2934],{"type":32,"value":794},{"type":27,"tag":790,"props":2936,"children":2937},{},[2938],{"type":32,"value":799},{"type":27,"tag":790,"props":2940,"children":2941},{},[2942],{"type":32,"value":804},{"type":27,"tag":790,"props":2944,"children":2945},{},[2946],{"type":32,"value":809},{"type":27,"tag":758,"props":2948,"children":2949},{},[2950,2954,2958,2962],{"type":27,"tag":790,"props":2951,"children":2952},{},[2953],{"type":32,"value":14},{"type":27,"tag":790,"props":2955,"children":2956},{},[2957],{"type":32,"value":821},{"type":27,"tag":790,"props":2959,"children":2960},{},[2961],{"type":32,"value":826},{"type":27,"tag":790,"props":2963,"children":2964},{},[2965],{"type":32,"value":831},{"type":27,"tag":49,"props":2967,"children":2968},{"id":834},[2969],{"type":32,"value":834},{"type":27,"tag":28,"props":2971,"children":2972},{},[2973],{"type":32,"value":841},{"type":27,"tag":28,"props":2975,"children":2976},{},[2977],{"type":32,"value":846},{"type":27,"tag":60,"props":2979,"children":2980},{},[2981,2997,3019],{"type":27,"tag":64,"props":2982,"children":2983},{},[2984,2985,2990,2991,2996],{"type":32,"value":854},{"type":27,"tag":40,"props":2986,"children":2988},{"className":2987},[],[2989],{"type":32,"value":84},{"type":32,"value":86},{"type":27,"tag":40,"props":2992,"children":2994},{"className":2993},[],[2995],{"type":32,"value":92},{"type":32,"value":867},{"type":27,"tag":64,"props":2998,"children":2999},{},[3000,3001,3006,3007,3012,3013,3018],{"type":32,"value":872},{"type":27,"tag":40,"props":3002,"children":3004},{"className":3003},[],[3005],{"type":32,"value":195},{"type":32,"value":879},{"type":27,"tag":40,"props":3008,"children":3010},{"className":3009},[],[3011],{"type":32,"value":262},{"type":32,"value":886},{"type":27,"tag":40,"props":3014,"children":3016},{"className":3015},[],[3017],{"type":32,"value":892},{"type":32,"value":894},{"type":27,"tag":64,"props":3020,"children":3021},{},[3022],{"type":32,"value":899},{"title":7,"searchDepth":901,"depth":901,"links":3024},[3025,3026,3027,3028,3029,3030,3031,3032,3033],{"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},1782088273768]