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