[{"data":1,"prerenderedAt":2421},["ShallowReactive",2],{"article-/topics/typescript/typescript-generic-constraints-conditional-decision-guide":3,"related-typescript":581,"content-query-U9BTtdfQaE":1980},{"_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":575,"_id":576,"_source":577,"_file":578,"_stem":579,"_extension":580},"/topics/typescript/typescript-generic-constraints-conditional-decision-guide","typescript",false,"","TypeScript 泛型约束与条件泛型的实际决策：什么时候用 extends、重载还是条件类型","泛型约束、函数重载和条件类型都能处理","2026-06-04","HTMLPAGE 团队",[13,14,15,16,17],"TypeScript","泛型约束","函数重载","条件类型","类型推导","/images/articles/typescript-generic-constraints-conditional-decision-guide-featured.jpg","typescript generic constraints decision code laptop",34804003,"https://www.pexels.com/photo/modern-workspace-with-laptop-and-code-display-34804003/",18,{"type":24,"children":25,"toc":559},"root",[26,43,48,54,65,70,103,109,114,123,128,137,158,164,169,178,191,200,236,241,259,265,270,279,284,290,295,304,309,328,357,362,367,375,380,387,396,401,407,416,421,427,436,441,446,554],{"type":27,"tag":28,"props":29,"children":30},"element","p",{},[31,34,41],{"type":32,"value":33},"text","TypeScript 有很多方式来处理\"相同的函数在不同输入类型下表现不同\"的场景。泛型约束（",{"type":27,"tag":35,"props":36,"children":38},"code",{"className":37},[],[39],{"type":32,"value":40},"extends",{"type":32,"value":42},"）限制类型范围，函数重载（overload）定义多个签名，条件类型（conditional types）在类型层面做分支。很多人会用其中一两种，但很少想清楚什么时候该用哪一种。",{"type":27,"tag":28,"props":44,"children":45},{},[46],{"type":32,"value":47},"这三种机制不是互斥的——它们经常混用。但选错了工具会导致类型推导不准确、可读性差或维护成本高。",{"type":27,"tag":49,"props":50,"children":52},"h2",{"id":51},"三种机制的定位差异",[53],{"type":32,"value":51},{"type":27,"tag":55,"props":56,"children":60},"pre",{"code":57,"language":5,"meta":7,"className":58},"// 泛型约束：限制输入的类型范围\nfunction identity\u003CT extends string | number>(x: T): T {\n  return x\n}\n\n// 函数重载：同一个函数的不同签名\nfunction handle(x: string): string\nfunction handle(x: number): number\nfunction handle(x: string | number): string | number {\n  return x // 实现签名必须兼容所有重载\n}\n\n// 条件类型：根据输入类型决定输出类型\ntype Output\u003CT> = T extends string ? 'text' : T extends number ? 'num' : 'other'\n",[59],"language-typescript",[61],{"type":27,"tag":35,"props":62,"children":63},{"__ignoreMap":7},[64],{"type":32,"value":57},{"type":27,"tag":28,"props":66,"children":67},{},[68],{"type":32,"value":69},"简单来说：",{"type":27,"tag":71,"props":72,"children":73},"ul",{},[74,85,94],{"type":27,"tag":75,"props":76,"children":77},"li",{},[78,83],{"type":27,"tag":79,"props":80,"children":81},"strong",{},[82],{"type":32,"value":14},{"type":32,"value":84},"说\"我只接受某种类型\"",{"type":27,"tag":75,"props":86,"children":87},{},[88,92],{"type":27,"tag":79,"props":89,"children":90},{},[91],{"type":32,"value":15},{"type":32,"value":93},"说\"你的输入和输出之间有确定的对应关系\"",{"type":27,"tag":75,"props":95,"children":96},{},[97,101],{"type":27,"tag":79,"props":98,"children":99},{},[100],{"type":32,"value":16},{"type":32,"value":102},"说\"输出类型由输入类型的特征决定\"",{"type":27,"tag":49,"props":104,"children":106},{"id":105},"场景-1限制入参但返回值和入参类型一致",[107],{"type":32,"value":108},"场景 1：限制入参，但返回值和入参类型一致",{"type":27,"tag":28,"props":110,"children":111},{},[112],{"type":32,"value":113},"这是泛型约束最直接的用法：",{"type":27,"tag":55,"props":115,"children":118},{"code":116,"language":5,"meta":7,"className":117},"function getLength\u003CT extends { length: number }>(arg: T): number {\n  return arg.length\n}\n\ngetLength('hello')     // ✅ —— string 有 length\ngetLength([1, 2, 3])   // ✅ —— array 有 length\ngetLength(42)          // ❌ —— number 没有 length\n",[59],[119],{"type":27,"tag":35,"props":120,"children":121},{"__ignoreMap":7},[122],{"type":32,"value":116},{"type":27,"tag":28,"props":124,"children":125},{},[126],{"type":32,"value":127},"这里用重载也能实现同样效果，但重载需要逐个列出可接受的类型：",{"type":27,"tag":55,"props":129,"children":132},{"code":130,"language":5,"meta":7,"className":131},"function getLengthOverload(arg: string): number\nfunction getLengthOverload(arg: any[]): number\nfunction getLengthOverload(arg: { length: number }): number {\n  return arg.length\n}\n",[59],[133],{"type":27,"tag":35,"props":134,"children":135},{"__ignoreMap":7},[136],{"type":32,"value":130},{"type":27,"tag":28,"props":138,"children":139},{},[140,142,148,150,156],{"type":32,"value":141},"多了一个签名就多一行重载。如果之后要支持 ",{"type":27,"tag":35,"props":143,"children":145},{"className":144},[],[146],{"type":32,"value":147},"ArrayBuffer",{"type":32,"value":149}," 或其他有 ",{"type":27,"tag":35,"props":151,"children":153},{"className":152},[],[154],{"type":32,"value":155},"length",{"type":32,"value":157}," 的类型，泛型约束不需要改代码，重载则需要加新签名。所以当\"能接受什么类型\"可以由结构特征描述时，泛型约束比重载更灵活。",{"type":27,"tag":49,"props":159,"children":161},{"id":160},"场景-2输入输出之间有确定的类型映射",[162],{"type":32,"value":163},"场景 2：输入输出之间有确定的类型映射",{"type":27,"tag":28,"props":165,"children":166},{},[167],{"type":32,"value":168},"重载的典型场景是\"传入 string 返回 string，传入 number 返回 number\"：",{"type":27,"tag":55,"props":170,"children":173},{"code":171,"language":5,"meta":7,"className":172},"function double\u003CT extends number | string>(x: T): T extends string ? string : number\nfunction double(x: any): any {\n  if (typeof x === 'string') return x + x\n  return x * 2\n}\n",[59],[174],{"type":27,"tag":35,"props":175,"children":176},{"__ignoreMap":7},[177],{"type":32,"value":171},{"type":27,"tag":28,"props":179,"children":180},{},[181,183,189],{"type":32,"value":182},"这里用了一个条件类型的返回类型，但实现上需要写 ",{"type":27,"tag":35,"props":184,"children":186},{"className":185},[],[187],{"type":32,"value":188},"any",{"type":32,"value":190}," 转换。更常见的选择是函数重载：",{"type":27,"tag":55,"props":192,"children":195},{"code":193,"language":5,"meta":7,"className":194},"function double(x: string): string\nfunction double(x: number): number\nfunction double(x: any): any {\n  if (typeof x === 'string') return x + x\n  return x * 2\n}\n",[59],[196],{"type":27,"tag":35,"props":197,"children":198},{"__ignoreMap":7},[199],{"type":32,"value":193},{"type":27,"tag":28,"props":201,"children":202},{},[203,205,211,213,219,221,227,228,234],{"type":32,"value":204},"重载在这里的优势是：编译器知道 ",{"type":27,"tag":35,"props":206,"children":208},{"className":207},[],[209],{"type":32,"value":210},"double('a')",{"type":32,"value":212}," 返回 ",{"type":27,"tag":35,"props":214,"children":216},{"className":215},[],[217],{"type":32,"value":218},"string",{"type":32,"value":220},"，",{"type":27,"tag":35,"props":222,"children":224},{"className":223},[],[225],{"type":32,"value":226},"double(2)",{"type":32,"value":212},{"type":27,"tag":35,"props":229,"children":231},{"className":230},[],[232],{"type":32,"value":233},"number",{"type":32,"value":235},"。而且条件类型做返回类型时，在实现里经常需要类型断言。",{"type":27,"tag":28,"props":237,"children":238},{},[239],{"type":32,"value":240},"什么时候用重载而不是条件类型：",{"type":27,"tag":71,"props":242,"children":243},{},[244,249,254],{"type":27,"tag":75,"props":245,"children":246},{},[247],{"type":32,"value":248},"重载数量有限（一般 2-4 个）",{"type":27,"tag":75,"props":250,"children":251},{},[252],{"type":32,"value":253},"每个重载的输入输出关系是确定的、可枚举的",{"type":27,"tag":75,"props":255,"children":256},{},[257],{"type":32,"value":258},"重载签名之间的互斥性不需要额外逻辑保证",{"type":27,"tag":49,"props":260,"children":262},{"id":261},"场景-3输出类型由输入的结构特征决定",[263],{"type":32,"value":264},"场景 3：输出类型由输入的结构特征决定",{"type":27,"tag":28,"props":266,"children":267},{},[268],{"type":32,"value":269},"当输出类型不是\"输入 A 对应输出 B\"这种一对一的映射，而是需要根据输入的结构特征计算时，条件类型就上场了：",{"type":27,"tag":55,"props":271,"children":274},{"code":272,"language":5,"meta":7,"className":273},"type Flatten\u003CT> = T extends Array\u003Cinfer U> ? U : T\n\ntype Flat1 = Flatten\u003Cstring[]>    // string\ntype Flat2 = Flatten\u003Cnumber>      // number\n\ntype ElementType\u003CT> = T extends Array\u003Cinfer E> ? E : never\ntype E1 = ElementType\u003Cnumber[]>    // number\ntype E2 = ElementType\u003Cstring>      // never\n",[59],[275],{"type":27,"tag":35,"props":276,"children":277},{"__ignoreMap":7},[278],{"type":32,"value":272},{"type":27,"tag":28,"props":280,"children":281},{},[282],{"type":32,"value":283},"条件类型在这里做的是类型层面的\"if 判断\"——如果 T 是数组，取出元素类型；否则返回 T 本身。重载做不了这个，因为重载需要枚举所有输入，而条件类型只需要描述判断规则。",{"type":27,"tag":49,"props":285,"children":287},{"id":286},"场景-4泛型约束-条件类型的组合",[288],{"type":32,"value":289},"场景 4：泛型约束 + 条件类型的组合",{"type":27,"tag":28,"props":291,"children":292},{},[293],{"type":32,"value":294},"实际情况里，三个工具经常一起用。来看一个\"深度合并对象\"的类型定义：",{"type":27,"tag":55,"props":296,"children":299},{"code":297,"language":5,"meta":7,"className":298},"type Merge\u003CA, B> = {\n  [K in keyof A | keyof B]: K extends keyof A & keyof B\n    ? A[K] extends Record\u003Cstring, any>\n      ? B[K] extends Record\u003Cstring, any>\n        ? Merge\u003CA[K], B[K]>\n        : A[K] | B[K]\n      : A[K] | B[K]\n    : K extends keyof A\n      ? A[K]\n      : K extends keyof B\n        ? B[K]\n        : never\n}\n",[59],[300],{"type":27,"tag":35,"props":301,"children":302},{"__ignoreMap":7},[303],{"type":32,"value":297},{"type":27,"tag":28,"props":305,"children":306},{},[307],{"type":32,"value":308},"这个类型做了：",{"type":27,"tag":310,"props":311,"children":312},"ol",{},[313,318,323],{"type":27,"tag":75,"props":314,"children":315},{},[316],{"type":32,"value":317},"取并集键",{"type":27,"tag":75,"props":319,"children":320},{},[321],{"type":32,"value":322},"如果键在 A 和 B 中都存在，且值都是对象，递归合并",{"type":27,"tag":75,"props":324,"children":325},{},[326],{"type":32,"value":327},"如果只在一方存在，取对应值类型",{"type":27,"tag":28,"props":329,"children":330},{},[331,333,339,341,347,349,355],{"type":32,"value":332},"这里的泛型约束是隐式的——",{"type":27,"tag":35,"props":334,"children":336},{"className":335},[],[337],{"type":32,"value":338},"A",{"type":32,"value":340}," 和 ",{"type":27,"tag":35,"props":342,"children":344},{"className":343},[],[345],{"type":32,"value":346},"B",{"type":32,"value":348}," 被约束为 ",{"type":27,"tag":35,"props":350,"children":352},{"className":351},[],[353],{"type":32,"value":354},"Record\u003Cstring, any>",{"type":32,"value":356}," 的扩展（因为映射类型只作用于对象）。",{"type":27,"tag":49,"props":358,"children":360},{"id":359},"决策流程",[361],{"type":32,"value":359},{"type":27,"tag":28,"props":363,"children":364},{},[365],{"type":32,"value":366},"三种机制的选择可以按这个流程判断：",{"type":27,"tag":55,"props":368,"children":370},{"code":369},"函数需要处理多种输入类型？\n├─ 输入类型可以按结构特征约束？\n│  └─ 是 → 用泛型约束 \u003CT extends { ... }>\n│     └─ 输出和输入有关系？\n│        ├─ 一对一的枚举关系（少于 5 种）→ 函数重载\n│        └─ 由输入特征动态决定 → 条件类型\n└─ 否 → 直接写联合类型入参，用 typeof 做运行时判断\n",[371],{"type":27,"tag":35,"props":372,"children":373},{"__ignoreMap":7},[374],{"type":32,"value":369},{"type":27,"tag":49,"props":376,"children":378},{"id":377},"常见错误",[379],{"type":32,"value":377},{"type":27,"tag":381,"props":382,"children":384},"h3",{"id":383},"错误-1用泛型约束替代参数类型",[385],{"type":32,"value":386},"错误 1：用泛型约束替代参数类型",{"type":27,"tag":55,"props":388,"children":391},{"code":389,"language":5,"meta":7,"className":390},"// 错误写法：泛型约束不必要\nfunction getProp\u003CT extends Record\u003Cstring, any>>(obj: T, key: string): any {\n  return obj[key]\n}\n\n// 更清晰的写法：直接用参数类型\nfunction getProp(obj: Record\u003Cstring, any>, key: string): any {\n  return obj[key]\n}\n",[59],[392],{"type":27,"tag":35,"props":393,"children":394},{"__ignoreMap":7},[395],{"type":32,"value":389},{"type":27,"tag":28,"props":397,"children":398},{},[399],{"type":32,"value":400},"多余的泛型增加了类型参数数量，但没有增加类型安全。只有当返回值类型和输入类型有明确关系时，泛型才有意义。",{"type":27,"tag":381,"props":402,"children":404},{"id":403},"错误-2重载不足时滥用条件类型做返回类型",[405],{"type":32,"value":406},"错误 2：重载不足时滥用条件类型做返回类型",{"type":27,"tag":55,"props":408,"children":411},{"code":409,"language":5,"meta":7,"className":410},"// 过度设计\ntype ReturnType\u003CT> = T extends string ? string : number\n\nfunction process\u003CT>(x: T): ReturnType\u003CT>\nfunction process(x: any): any {\n  return typeof x === 'string' ? x : 42\n}\n\n// 更直接\nfunction process(x: string): string\nfunction process(x: number): number\nfunction process(x: any): any {\n  return typeof x === 'string' ? x : 42\n}\n",[59],[412],{"type":27,"tag":35,"props":413,"children":414},{"__ignoreMap":7},[415],{"type":32,"value":409},{"type":27,"tag":28,"props":417,"children":418},{},[419],{"type":32,"value":420},"条件类型做返回类型时，实现里仍然需要写宽类型 + 类型断言，重载反而更直接。",{"type":27,"tag":381,"props":422,"children":424},{"id":423},"错误-3泛型约束太宽或太窄",[425],{"type":32,"value":426},"错误 3：泛型约束太宽或太窄",{"type":27,"tag":55,"props":428,"children":431},{"code":429,"language":5,"meta":7,"className":430},"// 太宽——任何类型都符合\nfunction bad\u003CT>(x: T): T\n\n// 太窄——失去了泛型的意义\nfunction bad2\u003CT extends 'a' | 'b' | 'c'>(x: T): T\n\n// 合适——刚好能反映需要的结构\nfunction good\u003CT extends string | number>(x: T): string\n",[59],[432],{"type":27,"tag":35,"props":433,"children":434},{"__ignoreMap":7},[435],{"type":32,"value":429},{"type":27,"tag":28,"props":437,"children":438},{},[439],{"type":32,"value":440},"泛型约束的理想粒度是\"刚好排除不需要的类型，又不止包含一个具体类型\"。",{"type":27,"tag":49,"props":442,"children":444},{"id":443},"总结",[445],{"type":32,"value":443},{"type":27,"tag":447,"props":448,"children":449},"table",{},[450,474],{"type":27,"tag":451,"props":452,"children":453},"thead",{},[454],{"type":27,"tag":455,"props":456,"children":457},"tr",{},[458,464,469],{"type":27,"tag":459,"props":460,"children":461},"th",{},[462],{"type":32,"value":463},"需求",{"type":27,"tag":459,"props":465,"children":466},{},[467],{"type":32,"value":468},"选用方式",{"type":27,"tag":459,"props":470,"children":471},{},[472],{"type":32,"value":473},"理由",{"type":27,"tag":475,"props":476,"children":477},"tbody",{},[478,502,519,536],{"type":27,"tag":455,"props":479,"children":480},{},[481,487,497],{"type":27,"tag":482,"props":483,"children":484},"td",{},[485],{"type":32,"value":486},"限制入参范围",{"type":27,"tag":482,"props":488,"children":489},{},[490,492],{"type":32,"value":491},"泛型约束 ",{"type":27,"tag":35,"props":493,"children":495},{"className":494},[],[496],{"type":32,"value":40},{"type":27,"tag":482,"props":498,"children":499},{},[500],{"type":32,"value":501},"结构兼容，不需要逐个枚举",{"type":27,"tag":455,"props":503,"children":504},{},[505,510,514],{"type":27,"tag":482,"props":506,"children":507},{},[508],{"type":32,"value":509},"有限数量签名映射",{"type":27,"tag":482,"props":511,"children":512},{},[513],{"type":32,"value":15},{"type":27,"tag":482,"props":515,"children":516},{},[517],{"type":32,"value":518},"声明式，类型推导准确",{"type":27,"tag":455,"props":520,"children":521},{},[522,527,531],{"type":27,"tag":482,"props":523,"children":524},{},[525],{"type":32,"value":526},"类型级分支计算",{"type":27,"tag":482,"props":528,"children":529},{},[530],{"type":32,"value":16},{"type":27,"tag":482,"props":532,"children":533},{},[534],{"type":32,"value":535},"灵活，可组合递归",{"type":27,"tag":455,"props":537,"children":538},{},[539,544,549],{"type":27,"tag":482,"props":540,"children":541},{},[542],{"type":32,"value":543},"泛型+重载+条件",{"type":27,"tag":482,"props":545,"children":546},{},[547],{"type":32,"value":548},"组合使用",{"type":27,"tag":482,"props":550,"children":551},{},[552],{"type":32,"value":553},"复杂签名场景的常规做法",{"type":27,"tag":28,"props":555,"children":556},{},[557],{"type":32,"value":558},"选工具时先问一个问题：这个类型逻辑是\"枚举\"还是\"判断\"？枚举用重载，判断用条件类型，限制用泛型约束。",{"title":7,"searchDepth":560,"depth":560,"links":561},3,[562,564,565,566,567,568,569,574],{"id":51,"depth":563,"text":51},2,{"id":105,"depth":563,"text":108},{"id":160,"depth":563,"text":163},{"id":261,"depth":563,"text":264},{"id":286,"depth":563,"text":289},{"id":359,"depth":563,"text":359},{"id":377,"depth":563,"text":377,"children":570},[571,572,573],{"id":383,"depth":560,"text":386},{"id":403,"depth":560,"text":406},{"id":423,"depth":560,"text":426},{"id":443,"depth":563,"text":443},"markdown","content:topics:typescript:typescript-generic-constraints-conditional-decision-guide.md","content","topics/typescript/typescript-generic-constraints-conditional-decision-guide.md","topics/typescript/typescript-generic-constraints-conditional-decision-guide","md",[582,1018,1444],{"_path":583,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":584,"description":585,"date":586,"topic":5,"author":11,"tags":587,"image":592,"featured":593,"readingTime":594,"body":595,"_type":575,"_id":1015,"_source":577,"_file":1016,"_stem":1017,"_extension":580},"/topics/typescript/typescript-vue3-best-practices","TypeScript 在 Vue 3 中的最佳实践","深度讲解如何在 Vue 3 中高效使用 TypeScript，包括类型定义、接口设计、generics 应用、常见错误等完整指南。","2025-12-27",[13,588,589,590,591],"Vue 3","类型安全","最佳实践","接口设计","/images/topics/typescript-vue3.jpg",true,12,{"type":24,"children":596,"toc":988},[597,602,607,613,619,628,634,643,649,654,663,668,677,682,691,697,703,712,718,727,733,739,748,754,763,769,778,784,793,799,808,814,823,829,838,842,847,948,953],{"type":27,"tag":49,"props":598,"children":600},{"id":599},"typescript-在-vue-3-中的最佳实践",[601],{"type":32,"value":584},{"type":27,"tag":28,"props":603,"children":604},{},[605],{"type":32,"value":606},"TypeScript 让 Vue 开发更加安全可靠。本文讲解如何在 Vue 3 中高效使用 TypeScript。",{"type":27,"tag":49,"props":608,"children":610},{"id":609},"_1-基础类型定义",[611],{"type":32,"value":612},"1. 基础类型定义",{"type":27,"tag":381,"props":614,"children":616},{"id":615},"组件-props-的类型定义",[617],{"type":32,"value":618},"组件 Props 的类型定义",{"type":27,"tag":55,"props":620,"children":623},{"className":621,"code":622,"language":5,"meta":7},[59],"// ✅ 完整的类型定义\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",[624],{"type":27,"tag":35,"props":625,"children":626},{"__ignoreMap":7},[627],{"type":32,"value":622},{"type":27,"tag":381,"props":629,"children":631},{"id":630},"组件-emits-的类型定义",[632],{"type":32,"value":633},"组件 Emits 的类型定义",{"type":27,"tag":55,"props":635,"children":638},{"className":636,"code":637,"language":5,"meta":7},[59],"// ✅ 类型安全的事件发射\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",[639],{"type":27,"tag":35,"props":640,"children":641},{"__ignoreMap":7},[642],{"type":32,"value":637},{"type":27,"tag":49,"props":644,"children":646},{"id":645},"_2-高级类型模式",[647],{"type":32,"value":648},"2. 高级类型模式",{"type":27,"tag":381,"props":650,"children":652},{"id":651},"泛型组件",[653],{"type":32,"value":651},{"type":27,"tag":55,"props":655,"children":658},{"className":656,"code":657,"language":5,"meta":7},[59],"// 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",[659],{"type":27,"tag":35,"props":660,"children":661},{"__ignoreMap":7},[662],{"type":32,"value":657},{"type":27,"tag":381,"props":664,"children":666},{"id":665},"条件类型和分布式条件类型",[667],{"type":32,"value":665},{"type":27,"tag":55,"props":669,"children":672},{"className":670,"code":671,"language":5,"meta":7},[59],"// 条件类型 (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",[673],{"type":27,"tag":35,"props":674,"children":675},{"__ignoreMap":7},[676],{"type":32,"value":671},{"type":27,"tag":381,"props":678,"children":680},{"id":679},"复杂的接口设计",[681],{"type":32,"value":679},{"type":27,"tag":55,"props":683,"children":686},{"className":684,"code":685,"language":5,"meta":7},[59],"// 实战: 表单验证框架\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",[687],{"type":27,"tag":35,"props":688,"children":689},{"__ignoreMap":7},[690],{"type":32,"value":685},{"type":27,"tag":49,"props":692,"children":694},{"id":693},"_3-组合式-api-的类型定义",[695],{"type":32,"value":696},"3. 组合式 API 的类型定义",{"type":27,"tag":381,"props":698,"children":700},{"id":699},"composable-的返回类型",[701],{"type":32,"value":702},"Composable 的返回类型",{"type":27,"tag":55,"props":704,"children":707},{"className":705,"code":706,"language":5,"meta":7},[59],"// 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",[708],{"type":27,"tag":35,"props":709,"children":710},{"__ignoreMap":7},[711],{"type":32,"value":706},{"type":27,"tag":381,"props":713,"children":715},{"id":714},"composable-的泛型",[716],{"type":32,"value":717},"Composable 的泛型",{"type":27,"tag":55,"props":719,"children":722},{"className":720,"code":721,"language":5,"meta":7},[59],"// 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",[723],{"type":27,"tag":35,"props":724,"children":725},{"__ignoreMap":7},[726],{"type":32,"value":721},{"type":27,"tag":49,"props":728,"children":730},{"id":729},"_4-常见类型错误和解决方案",[731],{"type":32,"value":732},"4. 常见类型错误和解决方案",{"type":27,"tag":381,"props":734,"children":736},{"id":735},"错误-1-any-类型滥用",[737],{"type":32,"value":738},"错误 1: Any 类型滥用",{"type":27,"tag":55,"props":740,"children":743},{"className":741,"code":742,"language":5,"meta":7},[59],"// ❌ 避免\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",[744],{"type":27,"tag":35,"props":745,"children":746},{"__ignoreMap":7},[747],{"type":32,"value":742},{"type":27,"tag":381,"props":749,"children":751},{"id":750},"错误-2-类型断言滥用",[752],{"type":32,"value":753},"错误 2: 类型断言滥用",{"type":27,"tag":55,"props":755,"children":758},{"className":756,"code":757,"language":5,"meta":7},[59],"// ❌ 避免 (类型断言隐藏问题)\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",[759],{"type":27,"tag":35,"props":760,"children":761},{"__ignoreMap":7},[762],{"type":32,"value":757},{"type":27,"tag":381,"props":764,"children":766},{"id":765},"错误-3-props-类型和运行时定义不匹配",[767],{"type":32,"value":768},"错误 3: Props 类型和运行时定义不匹配",{"type":27,"tag":55,"props":770,"children":773},{"className":771,"code":772,"language":5,"meta":7},[59],"// ❌ 避免 (类型和运行时不一致)\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",[774],{"type":27,"tag":35,"props":775,"children":776},{"__ignoreMap":7},[777],{"type":32,"value":772},{"type":27,"tag":49,"props":779,"children":781},{"id":780},"_5-pinia-store-的类型定义",[782],{"type":32,"value":783},"5. Pinia Store 的类型定义",{"type":27,"tag":55,"props":785,"children":788},{"className":786,"code":787,"language":5,"meta":7},[59],"// 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",[789],{"type":27,"tag":35,"props":790,"children":791},{"__ignoreMap":7},[792],{"type":32,"value":787},{"type":27,"tag":49,"props":794,"children":796},{"id":795},"_6-api-响应的类型定义",[797],{"type":32,"value":798},"6. API 响应的类型定义",{"type":27,"tag":55,"props":800,"children":803},{"className":801,"code":802,"language":5,"meta":7},[59],"// 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",[804],{"type":27,"tag":35,"props":805,"children":806},{"__ignoreMap":7},[807],{"type":32,"value":802},{"type":27,"tag":49,"props":809,"children":811},{"id":810},"_7-类型工具函数",[812],{"type":32,"value":813},"7. 类型工具函数",{"type":27,"tag":55,"props":815,"children":818},{"className":816,"code":817,"language":5,"meta":7},[59],"// 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",[819],{"type":27,"tag":35,"props":820,"children":821},{"__ignoreMap":7},[822],{"type":32,"value":817},{"type":27,"tag":49,"props":824,"children":826},{"id":825},"_8-最佳实践总结",[827],{"type":32,"value":828},"8. 最佳实践总结",{"type":27,"tag":55,"props":830,"children":833},{"className":831,"code":832,"language":5,"meta":7},[59],"// ✅ 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",[834],{"type":27,"tag":35,"props":835,"children":836},{"__ignoreMap":7},[837],{"type":32,"value":832},{"type":27,"tag":49,"props":839,"children":840},{"id":443},[841],{"type":32,"value":443},{"type":27,"tag":28,"props":843,"children":844},{},[845],{"type":32,"value":846},"TypeScript 在 Vue 3 中的优势：",{"type":27,"tag":447,"props":848,"children":849},{},[850,866],{"type":27,"tag":451,"props":851,"children":852},{},[853],{"type":27,"tag":455,"props":854,"children":855},{},[856,861],{"type":27,"tag":459,"props":857,"children":858},{},[859],{"type":32,"value":860},"特性",{"type":27,"tag":459,"props":862,"children":863},{},[864],{"type":32,"value":865},"优势",{"type":27,"tag":475,"props":867,"children":868},{},[869,884,900,916,932],{"type":27,"tag":455,"props":870,"children":871},{},[872,879],{"type":27,"tag":482,"props":873,"children":874},{},[875],{"type":27,"tag":79,"props":876,"children":877},{},[878],{"type":32,"value":589},{"type":27,"tag":482,"props":880,"children":881},{},[882],{"type":32,"value":883},"编译时发现错误",{"type":27,"tag":455,"props":885,"children":886},{},[887,895],{"type":27,"tag":482,"props":888,"children":889},{},[890],{"type":27,"tag":79,"props":891,"children":892},{},[893],{"type":32,"value":894},"自动补全",{"type":27,"tag":482,"props":896,"children":897},{},[898],{"type":32,"value":899},"IDE 提示更准确",{"type":27,"tag":455,"props":901,"children":902},{},[903,911],{"type":27,"tag":482,"props":904,"children":905},{},[906],{"type":27,"tag":79,"props":907,"children":908},{},[909],{"type":32,"value":910},"可维护性",{"type":27,"tag":482,"props":912,"children":913},{},[914],{"type":32,"value":915},"代码意图更明确",{"type":27,"tag":455,"props":917,"children":918},{},[919,927],{"type":27,"tag":482,"props":920,"children":921},{},[922],{"type":27,"tag":79,"props":923,"children":924},{},[925],{"type":32,"value":926},"重构安全",{"type":27,"tag":482,"props":928,"children":929},{},[930],{"type":32,"value":931},"改变代码有反馈",{"type":27,"tag":455,"props":933,"children":934},{},[935,943],{"type":27,"tag":482,"props":936,"children":937},{},[938],{"type":27,"tag":79,"props":939,"children":940},{},[941],{"type":32,"value":942},"文档作用",{"type":27,"tag":482,"props":944,"children":945},{},[946],{"type":32,"value":947},"类型即文档",{"type":27,"tag":49,"props":949,"children":951},{"id":950},"相关资源",[952],{"type":32,"value":950},{"type":27,"tag":71,"props":954,"children":955},{},[956,968,978],{"type":27,"tag":75,"props":957,"children":958},{},[959],{"type":27,"tag":960,"props":961,"children":965},"a",{"href":962,"rel":963},"https://www.typescriptlang.org/docs/",[964],"nofollow",[966],{"type":32,"value":967},"TypeScript 官方手册",{"type":27,"tag":75,"props":969,"children":970},{},[971],{"type":27,"tag":960,"props":972,"children":975},{"href":973,"rel":974},"https://vuejs.org/guide/typescript/overview.html",[964],[976],{"type":32,"value":977},"Vue 3 TypeScript 指南",{"type":27,"tag":75,"props":979,"children":980},{},[981],{"type":27,"tag":960,"props":982,"children":985},{"href":983,"rel":984},"https://www.typescriptlang.org/docs/handbook/2/types-from-types.html",[964],[986],{"type":32,"value":987},"TypeScript 高级类型",{"title":7,"searchDepth":560,"depth":560,"links":989},[990,991,995,1000,1004,1009,1010,1011,1012,1013,1014],{"id":599,"depth":563,"text":584},{"id":609,"depth":563,"text":612,"children":992},[993,994],{"id":615,"depth":560,"text":618},{"id":630,"depth":560,"text":633},{"id":645,"depth":563,"text":648,"children":996},[997,998,999],{"id":651,"depth":560,"text":651},{"id":665,"depth":560,"text":665},{"id":679,"depth":560,"text":679},{"id":693,"depth":563,"text":696,"children":1001},[1002,1003],{"id":699,"depth":560,"text":702},{"id":714,"depth":560,"text":717},{"id":729,"depth":563,"text":732,"children":1005},[1006,1007,1008],{"id":735,"depth":560,"text":738},{"id":750,"depth":560,"text":753},{"id":765,"depth":560,"text":768},{"id":780,"depth":563,"text":783},{"id":795,"depth":563,"text":798},{"id":810,"depth":563,"text":813},{"id":825,"depth":563,"text":828},{"id":443,"depth":563,"text":443},{"id":950,"depth":563,"text":950},"content:topics:typescript:typescript-vue3-best-practices.md","topics/typescript/typescript-vue3-best-practices.md","topics/typescript/typescript-vue3-best-practices",{"_path":1019,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1020,"description":1021,"date":1022,"topic":5,"author":11,"tags":1023,"image":1028,"imageQuery":1029,"pexelsPhotoId":1030,"pexelsUrl":1031,"featured":6,"readingTime":1032,"body":1033,"_type":575,"_id":1441,"_source":577,"_file":1442,"_stem":1443,"_extension":580},"/topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy","TypeScript 设计模式实战：工厂、策略、适配器与代理怎样保持类型安全","设计模式在 TypeScript 里不该变成 class 套娃。本文从工厂、策略、适配器和代理四个高频场景出发，讲清如何让抽象保持灵活，同时不牺牲类型安全和可读性。","2026-06-08",[13,1024,1025,1026,1027],"Design Patterns","Factory Pattern","Strategy Pattern","Adapter Pattern","/images/articles/typescript-design-patterns-factory-strategy-adapter-proxy-featured.jpg","software design patterns code laptop",34804023,"https://www.pexels.com/photo/close-up-of-computer-screen-with-code-reflection-34804023/",17,{"type":24,"children":1034,"toc":1432},[1035,1040,1052,1058,1063,1074,1086,1092,1097,1106,1119,1125,1146,1155,1160,1166,1171,1180,1185,1191,1196,1231,1236,1242,1341,1345,1350,1355,1391,1396],{"type":27,"tag":28,"props":1036,"children":1037},{},[1038],{"type":32,"value":1039},"很多人一提到“设计模式 + TypeScript”，脑子里马上冒出一套厚重的 class 结构：抽象工厂、基类、接口、子类、再来几层继承。问题在于，前端和 Node 项目里真正需要的，往往不是把经典 OO 图谱原样搬进来，而是把“变化点”抽象出来，同时保证调用方还能读懂类型。",{"type":27,"tag":28,"props":1041,"children":1042},{},[1043,1045,1050],{"type":32,"value":1044},"TypeScript 在设计模式里的价值，不是让模式更像 Java，而是让模式更诚实。哪些输入是合法的、哪些策略实现必须覆盖、适配器是否真的完成字段转换、代理是否保留原函数签名，这些都可以由类型系统提前约束。如果模式一上来就把类型信息打成 ",{"type":27,"tag":35,"props":1046,"children":1048},{"className":1047},[],[1049],{"type":32,"value":188},{"type":32,"value":1051},"，那它带来的通常不是灵活，而是延后的风险。",{"type":27,"tag":49,"props":1053,"children":1055},{"id":1054},"工厂模式把怎么创建隔离出来但别把输入做成万能配置桶",[1056],{"type":32,"value":1057},"工厂模式：把“怎么创建”隔离出来，但别把输入做成万能配置桶",{"type":27,"tag":28,"props":1059,"children":1060},{},[1061],{"type":32,"value":1062},"工厂模式最适合解决创建逻辑和环境差异问题，比如：不同运行环境要创建不同客户端，不同支付方式要创建不同处理器。",{"type":27,"tag":55,"props":1064,"children":1069},{"className":1065,"code":1067,"language":1068,"meta":7},[1066],"language-ts","type StorageKind = 'memory' | 'redis'\n\ninterface Storage {\n  get(key: string): Promise\u003Cstring | null>\n  set(key: string, value: string): Promise\u003Cvoid>\n}\n\nfunction createStorage(kind: StorageKind): Storage {\n  switch (kind) {\n    case 'memory':\n      return createMemoryStorage()\n    case 'redis':\n      return createRedisStorage()\n  }\n}\n","ts",[1070],{"type":27,"tag":35,"props":1071,"children":1072},{"__ignoreMap":7},[1073],{"type":32,"value":1067},{"type":27,"tag":28,"props":1075,"children":1076},{},[1077,1079,1084],{"type":32,"value":1078},"真正要避免的，不是工厂本身，而是把工厂入参做成一个什么都能塞的 ",{"type":27,"tag":35,"props":1080,"children":1082},{"className":1081},[],[1083],{"type":32,"value":354},{"type":32,"value":1085},"。工厂模式一旦把输入边界放宽，调用方就会失去类型提示，后面连工厂是否真的支持某个组合都看不出来。",{"type":27,"tag":49,"props":1087,"children":1089},{"id":1088},"策略模式别用-if-else-链拖着业务跑把变化面显式枚举出来",[1090],{"type":32,"value":1091},"策略模式：别用 if else 链拖着业务跑，把变化面显式枚举出来",{"type":27,"tag":28,"props":1093,"children":1094},{},[1095],{"type":32,"value":1096},"策略模式最值钱的地方，是把“可替换规则”显式列出来，让新增策略变成扩展而不是改旧逻辑。",{"type":27,"tag":55,"props":1098,"children":1101},{"className":1099,"code":1100,"language":1068,"meta":7},[1066],"type PricingStrategy = 'standard' | 'vip' | 'promotion'\n\nconst pricingMap: Record\u003CPricingStrategy, (price: number) => number> = {\n  standard: (price) => price,\n  vip: (price) => price * 0.9,\n  promotion: (price) => price - 30\n}\n\nfunction calcPrice(strategy: PricingStrategy, basePrice: number): number {\n  return pricingMap[strategy](basePrice)\n}\n",[1102],{"type":27,"tag":35,"props":1103,"children":1104},{"__ignoreMap":7},[1105],{"type":32,"value":1100},{"type":27,"tag":28,"props":1107,"children":1108},{},[1109,1111,1117],{"type":32,"value":1110},"这里 ",{"type":27,"tag":35,"props":1112,"children":1114},{"className":1113},[],[1115],{"type":32,"value":1116},"Record\u003CPricingStrategy, ...>",{"type":32,"value":1118}," 的意义很大：只要你新增一个策略名，TypeScript 会强制你把实现补齐。这样策略模式不只是结构好看，而是具备穷举约束。",{"type":27,"tag":49,"props":1120,"children":1122},{"id":1121},"适配器模式真正重要的是把外部不稳定结构拦在边界外",[1123],{"type":32,"value":1124},"适配器模式：真正重要的是把外部不稳定结构拦在边界外",{"type":27,"tag":28,"props":1126,"children":1127},{},[1128,1130,1136,1138,1144],{"type":32,"value":1129},"适配器最常被低估。很多团队明明已经接了多个第三方服务，却还在业务层到处判断“这个平台字段叫 ",{"type":27,"tag":35,"props":1131,"children":1133},{"className":1132},[],[1134],{"type":32,"value":1135},"full_name",{"type":32,"value":1137},"，那个平台叫 ",{"type":27,"tag":35,"props":1139,"children":1141},{"className":1140},[],[1142],{"type":32,"value":1143},"displayName",{"type":32,"value":1145},"”。本质上，这就是缺了适配器层。",{"type":27,"tag":55,"props":1147,"children":1150},{"className":1148,"code":1149,"language":1068,"meta":7},[1066],"type VendorUser = {\n  uid: string\n  full_name: string\n  active_flag: 0 | 1\n}\n\ntype UserProfile = {\n  id: string\n  name: string\n  isActive: boolean\n}\n\nfunction adaptVendorUser(input: VendorUser): UserProfile {\n  return {\n    id: input.uid,\n    name: input.full_name,\n    isActive: input.active_flag === 1\n  }\n}\n",[1151],{"type":27,"tag":35,"props":1152,"children":1153},{"__ignoreMap":7},[1154],{"type":32,"value":1149},{"type":27,"tag":28,"props":1156,"children":1157},{},[1158],{"type":32,"value":1159},"适配器模式的关键，不是写一个转换函数，而是把外部世界的不稳定命名和字段结构限制在边界里，别让业务层长期直接面对这些差异。",{"type":27,"tag":49,"props":1161,"children":1163},{"id":1162},"代理模式包装额外行为时最怕把原始签名弄丢",[1164],{"type":32,"value":1165},"代理模式：包装额外行为时，最怕把原始签名弄丢",{"type":27,"tag":28,"props":1167,"children":1168},{},[1169],{"type":32,"value":1170},"代理模式常用于日志、缓存、权限检查和重试。它最常见的问题是：包装之后，原函数签名丢了，返回值也宽了，调用方只能面对一个模糊函数。",{"type":27,"tag":55,"props":1172,"children":1175},{"className":1173,"code":1174,"language":1068,"meta":7},[1066],"function withTiming\u003CTArgs extends unknown[], TResult>(\n  fn: (...args: TArgs) => Promise\u003CTResult>\n) {\n  return async (...args: TArgs): Promise\u003CTResult> => {\n    const start = performance.now()\n    try {\n      return await fn(...args)\n    } finally {\n      console.log('cost', performance.now() - start)\n    }\n  }\n}\n",[1176],{"type":27,"tag":35,"props":1177,"children":1178},{"__ignoreMap":7},[1179],{"type":32,"value":1174},{"type":27,"tag":28,"props":1181,"children":1182},{},[1183],{"type":32,"value":1184},"这类泛型代理的价值在于：你加了额外行为，但原始参数和返回值仍然保留下来。否则代理模式会把“附加控制”变成“类型信息丢失”。",{"type":27,"tag":49,"props":1186,"children":1188},{"id":1187},"一个常见失败案例模式是抽象了类型却退化成-any",[1189],{"type":32,"value":1190},"一个常见失败案例：模式是抽象了，类型却退化成 any",{"type":27,"tag":28,"props":1192,"children":1193},{},[1194],{"type":32,"value":1195},"很多项目在做所谓“模式升级”时，会出现一种反效果：结构更复杂了，类型却更差了。常见表现有：",{"type":27,"tag":71,"props":1197,"children":1198},{},[1199,1210,1215,1226],{"type":27,"tag":75,"props":1200,"children":1201},{},[1202,1204],{"type":32,"value":1203},"工厂接收 ",{"type":27,"tag":35,"props":1205,"children":1207},{"className":1206},[],[1208],{"type":32,"value":1209},"config: any",{"type":27,"tag":75,"props":1211,"children":1212},{},[1213],{"type":32,"value":1214},"策略表写成对象，但 key 没有联合类型约束",{"type":27,"tag":75,"props":1216,"children":1217},{},[1218,1220],{"type":32,"value":1219},"适配器只返回 ",{"type":27,"tag":35,"props":1221,"children":1223},{"className":1222},[],[1224],{"type":32,"value":1225},"Record\u003Cstring, unknown>",{"type":27,"tag":75,"props":1227,"children":1228},{},[1229],{"type":32,"value":1230},"代理函数包装后不保留原始签名",{"type":27,"tag":28,"props":1232,"children":1233},{},[1234],{"type":32,"value":1235},"这种代码看起来“更有架构感”，实际上却把很多风险从编译期挪回了运行时。模式如果不能提升边界清晰度和替换安全性，通常只是引入了新的复杂度。",{"type":27,"tag":49,"props":1237,"children":1239},{"id":1238},"决策表什么时候该上模式什么时候先别上",[1240],{"type":32,"value":1241},"决策表：什么时候该上模式，什么时候先别上",{"type":27,"tag":447,"props":1243,"children":1244},{},[1245,1266],{"type":27,"tag":451,"props":1246,"children":1247},{},[1248],{"type":27,"tag":455,"props":1249,"children":1250},{},[1251,1256,1261],{"type":27,"tag":459,"props":1252,"children":1253},{},[1254],{"type":32,"value":1255},"场景",{"type":27,"tag":459,"props":1257,"children":1258},{},[1259],{"type":32,"value":1260},"更适合什么模式",{"type":27,"tag":459,"props":1262,"children":1263},{},[1264],{"type":32,"value":1265},"不建议做的事",{"type":27,"tag":475,"props":1267,"children":1268},{},[1269,1287,1305,1323],{"type":27,"tag":455,"props":1270,"children":1271},{},[1272,1277,1282],{"type":27,"tag":482,"props":1273,"children":1274},{},[1275],{"type":32,"value":1276},"创建逻辑随环境变化",{"type":27,"tag":482,"props":1278,"children":1279},{},[1280],{"type":32,"value":1281},"工厂",{"type":27,"tag":482,"props":1283,"children":1284},{},[1285],{"type":32,"value":1286},"把所有可选项塞进一个大配置对象",{"type":27,"tag":455,"props":1288,"children":1289},{},[1290,1295,1300],{"type":27,"tag":482,"props":1291,"children":1292},{},[1293],{"type":32,"value":1294},"规则可替换、可扩展",{"type":27,"tag":482,"props":1296,"children":1297},{},[1298],{"type":32,"value":1299},"策略",{"type":27,"tag":482,"props":1301,"children":1302},{},[1303],{"type":32,"value":1304},"继续堆 if else 并靠注释解释",{"type":27,"tag":455,"props":1306,"children":1307},{},[1308,1313,1318],{"type":27,"tag":482,"props":1309,"children":1310},{},[1311],{"type":32,"value":1312},"第三方结构不稳定",{"type":27,"tag":482,"props":1314,"children":1315},{},[1316],{"type":32,"value":1317},"适配器",{"type":27,"tag":482,"props":1319,"children":1320},{},[1321],{"type":32,"value":1322},"让业务层到处处理字段差异",{"type":27,"tag":455,"props":1324,"children":1325},{},[1326,1331,1336],{"type":27,"tag":482,"props":1327,"children":1328},{},[1329],{"type":32,"value":1330},"需要附加日志、缓存、权限",{"type":27,"tag":482,"props":1332,"children":1333},{},[1334],{"type":32,"value":1335},"代理",{"type":27,"tag":482,"props":1337,"children":1338},{},[1339],{"type":32,"value":1340},"包装后丢失原函数签名",{"type":27,"tag":49,"props":1342,"children":1343},{"id":443},[1344],{"type":32,"value":443},{"type":27,"tag":28,"props":1346,"children":1347},{},[1348],{"type":32,"value":1349},"TypeScript 里的设计模式，关键不是“更像面向对象”，而是让变化面、稳定面和边界责任表达得更清楚。工厂控制创建，策略控制规则替换，适配器隔离外部差异，代理保留签名的同时叠加附加能力。只要模式引入后类型信息还在，团队就能真正享受到抽象带来的收益。",{"type":27,"tag":28,"props":1351,"children":1352},{},[1353],{"type":32,"value":1354},"本系列导航：",{"type":27,"tag":71,"props":1356,"children":1357},{},[1358,1369,1380],{"type":27,"tag":75,"props":1359,"children":1360},{},[1361,1363],{"type":32,"value":1362},"如果你想先稳住抽象层对外承诺，接着看 ",{"type":27,"tag":960,"props":1364,"children":1366},{"href":1365},"/topics/typescript/typescript-public-api-design-exported-types-breaking-change-control",[1367],{"type":32,"value":1368},"TypeScript 公共 API 设计",{"type":27,"tag":75,"props":1370,"children":1371},{},[1372,1374],{"type":32,"value":1373},"若你要把模式继续落到测试和用例上，再看 ",{"type":27,"tag":960,"props":1375,"children":1377},{"href":1376},"/topics/typescript/typescript-test-data-builders-fixtures-mock-type-safety",[1378],{"type":32,"value":1379},"TypeScript 测试数据构建",{"type":27,"tag":75,"props":1381,"children":1382},{},[1383,1385],{"type":32,"value":1384},"如果你接下来要处理业务状态与错误流，再读 ",{"type":27,"tag":960,"props":1386,"children":1388},{"href":1387},"/topics/typescript/typescript-form-state-validation-error-modeling-guide",[1389],{"type":32,"value":1390},"TypeScript 表单与错误状态建模",{"type":27,"tag":28,"props":1392,"children":1393},{},[1394],{"type":32,"value":1395},"延伸阅读：",{"type":27,"tag":71,"props":1397,"children":1398},{},[1399,1407,1416,1423],{"type":27,"tag":75,"props":1400,"children":1401},{},[1402],{"type":27,"tag":960,"props":1403,"children":1404},{"href":4},[1405],{"type":32,"value":1406},"TypeScript 泛型约束与条件泛型的实际决策",{"type":27,"tag":75,"props":1408,"children":1409},{},[1410],{"type":27,"tag":960,"props":1411,"children":1413},{"href":1412},"/topics/typescript/typescript-discriminated-unions-exhaustive-check-state-management",[1414],{"type":32,"value":1415},"TypeScript 可辨识联合与穷举检查",{"type":27,"tag":75,"props":1417,"children":1418},{},[1419],{"type":27,"tag":960,"props":1420,"children":1421},{"href":1365},[1422],{"type":32,"value":1368},{"type":27,"tag":75,"props":1424,"children":1425},{},[1426],{"type":27,"tag":960,"props":1427,"children":1429},{"href":1428},"/topics/frontend/vue-component-design-patterns-essentials",[1430],{"type":32,"value":1431},"Vue 组件设计模式精选",{"title":7,"searchDepth":560,"depth":560,"links":1433},[1434,1435,1436,1437,1438,1439,1440],{"id":1054,"depth":563,"text":1057},{"id":1088,"depth":563,"text":1091},{"id":1121,"depth":563,"text":1124},{"id":1162,"depth":563,"text":1165},{"id":1187,"depth":563,"text":1190},{"id":1238,"depth":563,"text":1241},{"id":443,"depth":563,"text":443},"content:topics:typescript:typescript-design-patterns-factory-strategy-adapter-proxy.md","topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy.md","topics/typescript/typescript-design-patterns-factory-strategy-adapter-proxy",{"_path":1445,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1446,"description":1447,"date":1022,"topic":5,"author":11,"tags":1448,"image":1453,"imageQuery":1454,"pexelsPhotoId":1455,"pexelsUrl":1456,"featured":6,"readingTime":1457,"body":1458,"_type":575,"_id":1977,"_source":577,"_file":1978,"_stem":1979,"_extension":580},"/topics/typescript/typescript-event-map-payload-contract-subscription-safety","TypeScript 事件系统建模：事件映射、payload 约束与订阅端类型安全","事件系统最容易失控的地方，不是发不出去，而是名字、payload 和订阅约定慢慢漂移。本文从 event map、发布订阅 API 设计和版本演进出发，讲清 TypeScript 如何让事件系统保持可协作。",[13,1449,1450,1451,1452],"Event Map","PubSub","Payload","Event Driven","/images/articles/typescript-event-map-payload-contract-subscription-safety-featured.jpg","event driven architecture code laptop",10826689,"https://www.pexels.com/photo/a-laptop-on-the-table-10826689/",16,{"type":24,"children":1459,"toc":1967},[1460,1497,1502,1508,1517,1522,1535,1540,1546,1551,1560,1565,1574,1579,1585,1590,1603,1615,1620,1638,1644,1649,1654,1672,1677,1683,1703,1721,1726,1732,1737,1760,1765,1770,1798,1802,1807,1812,1894,1898,1928,1932],{"type":27,"tag":28,"props":1461,"children":1462},{},[1463,1465,1471,1473,1479,1481,1487,1489,1495],{"type":32,"value":1464},"很多系统一开始的事件总线都很轻：",{"type":27,"tag":35,"props":1466,"children":1468},{"className":1467},[],[1469],{"type":32,"value":1470},"emit(name, payload)",{"type":32,"value":1472},"，再配一个 ",{"type":27,"tag":35,"props":1474,"children":1476},{"className":1475},[],[1477],{"type":32,"value":1478},"on(name, handler)",{"type":32,"value":1480}," 就能跑起来。真正的问题不会立刻出现，而是随着事件增多、订阅方变多、场景变复杂，名字和 payload 会慢慢失去同步。某个地方仍然发 ",{"type":27,"tag":35,"props":1482,"children":1484},{"className":1483},[],[1485],{"type":32,"value":1486},"user.updated",{"type":32,"value":1488},"，另一个地方已经开始监听 ",{"type":27,"tag":35,"props":1490,"children":1492},{"className":1491},[],[1493],{"type":32,"value":1494},"user.profile.updated",{"type":32,"value":1496},"；某次迭代里 payload 多了一个字段，但下游还按旧形态读取；有的 handler 以为收到的是单对象，另一个地方却开始传数组。",{"type":27,"tag":28,"props":1498,"children":1499},{},[1500],{"type":32,"value":1501},"TypeScript 在事件系统里的价值，不是把事件总线写得更花，而是把“事件名和 payload 的对应关系”变成可检查的契约。只要这一层没有被显式建模，系统后面再怎么拆模块、加队列、做回放，事件协作都很容易继续靠记忆和文档维持。",{"type":27,"tag":49,"props":1503,"children":1505},{"id":1504},"最危险的接口长这样事件名是-stringpayload-是-any",[1506],{"type":32,"value":1507},"最危险的接口长这样：事件名是 string，payload 是 any",{"type":27,"tag":55,"props":1509,"children":1512},{"className":1510,"code":1511,"language":1068,"meta":7},[1066],"function emit(event: string, payload: any) {\n  // ...\n}\n",[1513],{"type":27,"tag":35,"props":1514,"children":1515},{"__ignoreMap":7},[1516],{"type":32,"value":1511},{"type":27,"tag":28,"props":1518,"children":1519},{},[1520],{"type":32,"value":1521},"这类写法的坏处不是“没有类型提示”这么简单，而是它让系统的两个核心约束完全脱离了关系：",{"type":27,"tag":71,"props":1523,"children":1524},{},[1525,1530],{"type":27,"tag":75,"props":1526,"children":1527},{},[1528],{"type":32,"value":1529},"哪些事件名是合法的。",{"type":27,"tag":75,"props":1531,"children":1532},{},[1533],{"type":32,"value":1534},"某个事件对应的 payload 应该长什么样。",{"type":27,"tag":28,"props":1536,"children":1537},{},[1538],{"type":32,"value":1539},"只要这两个约束还靠人脑维护，规模一大就一定会漂移。",{"type":27,"tag":49,"props":1541,"children":1543},{"id":1542},"event-map-是最稳的起点先把名字和-payload-绑定起来",[1544],{"type":32,"value":1545},"event map 是最稳的起点：先把名字和 payload 绑定起来",{"type":27,"tag":28,"props":1547,"children":1548},{},[1549],{"type":32,"value":1550},"一种非常实用的建模方式，是先定义事件映射表：",{"type":27,"tag":55,"props":1552,"children":1555},{"className":1553,"code":1554,"language":1068,"meta":7},[1066],"type AppEventMap = {\n  'user.created': { id: string; source: 'admin' | 'self-service' }\n  'user.deleted': { id: string; reason?: string }\n  'invoice.paid': { invoiceId: string; amount: number }\n}\n",[1556],{"type":27,"tag":35,"props":1557,"children":1558},{"__ignoreMap":7},[1559],{"type":32,"value":1554},{"type":27,"tag":28,"props":1561,"children":1562},{},[1563],{"type":32,"value":1564},"一旦这张表存在，发布和订阅接口都可以围绕它推导：",{"type":27,"tag":55,"props":1566,"children":1569},{"className":1567,"code":1568,"language":1068,"meta":7},[1066],"function emit\u003CK extends keyof AppEventMap>(\n  event: K,\n  payload: AppEventMap[K]\n) {}\n\nfunction on\u003CK extends keyof AppEventMap>(\n  event: K,\n  handler: (payload: AppEventMap[K]) => void\n) {}\n",[1570],{"type":27,"tag":35,"props":1571,"children":1572},{"__ignoreMap":7},[1573],{"type":32,"value":1568},{"type":27,"tag":28,"props":1575,"children":1576},{},[1577],{"type":32,"value":1578},"这类 API 的意义不只是补全更舒服，而是让“事件名”和“事件负载”在类型层面无法脱钩。只要事件名选错、payload 形状不对，问题会在提交代码前就暴露。",{"type":27,"tag":49,"props":1580,"children":1582},{"id":1581},"事件设计真正要防的是同名异义和异名同义",[1583],{"type":32,"value":1584},"事件设计真正要防的是“同名异义”和“异名同义”",{"type":27,"tag":28,"props":1586,"children":1587},{},[1588],{"type":32,"value":1589},"事件系统最容易出现的两类混乱是：",{"type":27,"tag":71,"props":1591,"children":1592},{},[1593,1598],{"type":27,"tag":75,"props":1594,"children":1595},{},[1596],{"type":32,"value":1597},"同名异义：同一个事件名在不同模块里承载不同语义。",{"type":27,"tag":75,"props":1599,"children":1600},{},[1601],{"type":32,"value":1602},"异名同义：同一类业务事实被多个名字重复表达。",{"type":27,"tag":28,"props":1604,"children":1605},{},[1606,1608,1613],{"type":32,"value":1607},"比如 ",{"type":27,"tag":35,"props":1609,"children":1611},{"className":1610},[],[1612],{"type":32,"value":1486},{"type":32,"value":1614}," 这个名字，看起来什么都能装。用户改昵称、改角色、改手机号、改订阅偏好都能叫 updated。短期很方便，长期却让订阅方不知道自己到底该监听什么，也让 payload 越长越杂。",{"type":27,"tag":28,"props":1616,"children":1617},{},[1618],{"type":32,"value":1619},"更稳的做法往往是：",{"type":27,"tag":71,"props":1621,"children":1622},{},[1623,1628,1633],{"type":27,"tag":75,"props":1624,"children":1625},{},[1626],{"type":32,"value":1627},"名称按业务事实切分，而不是按“发生过变化”这种宽泛概念命名。",{"type":27,"tag":75,"props":1629,"children":1630},{},[1631],{"type":32,"value":1632},"payload 只携带当前事件需要承诺的最小信息。",{"type":27,"tag":75,"props":1634,"children":1635},{},[1636],{"type":32,"value":1637},"大量共享字段通过公共对象或 metadata 包装，而不是每个事件都随意展开。",{"type":27,"tag":49,"props":1639,"children":1641},{"id":1640},"订阅端类型安全的关键不在泛型而在-handler-语义是否稳定",[1642],{"type":32,"value":1643},"订阅端类型安全的关键，不在泛型，而在 handler 语义是否稳定",{"type":27,"tag":28,"props":1645,"children":1646},{},[1647],{"type":32,"value":1648},"发布端和订阅端的类型常常被讨论成“泛型能不能写出来”，其实更重要的问题是：handler 是否能基于事件名做出稳定假设。如果一个事件 payload 经常改、字段意义经常变，哪怕泛型写对了，订阅端也不会真正稳定。",{"type":27,"tag":28,"props":1650,"children":1651},{},[1652],{"type":32,"value":1653},"所以团队在设计 event map 时，最好把这几件事一起定清：",{"type":27,"tag":71,"props":1655,"children":1656},{},[1657,1662,1667],{"type":27,"tag":75,"props":1658,"children":1659},{},[1660],{"type":32,"value":1661},"这个事件代表什么业务事实。",{"type":27,"tag":75,"props":1663,"children":1664},{},[1665],{"type":32,"value":1666},"订阅方最少需要拿到哪些字段。",{"type":27,"tag":75,"props":1668,"children":1669},{},[1670],{"type":32,"value":1671},"哪些字段未来允许扩展，哪些字段一旦变化就算破坏式变更。",{"type":27,"tag":28,"props":1673,"children":1674},{},[1675],{"type":32,"value":1676},"TypeScript 能帮你守住结构，但不能替你定义清业务语义。",{"type":27,"tag":49,"props":1678,"children":1680},{"id":1679},"一个常见失败案例事件总线很通用但所有变更都在悄悄破约",[1681],{"type":32,"value":1682},"一个常见失败案例：事件总线很“通用”，但所有变更都在悄悄破约",{"type":27,"tag":28,"props":1684,"children":1685},{},[1686,1688,1694,1695,1701],{"type":32,"value":1687},"某团队有一套抽象得很漂亮的事件总线封装，",{"type":27,"tag":35,"props":1689,"children":1691},{"className":1690},[],[1692],{"type":32,"value":1693},"emit",{"type":32,"value":340},{"type":27,"tag":35,"props":1696,"children":1698},{"className":1697},[],[1699],{"type":32,"value":1700},"on",{"type":32,"value":1702}," 都做成了泛型，但事件名本身没有中心化建模，而是由各模块自己声明字符串字面量。结果几个月后出现了典型问题：",{"type":27,"tag":71,"props":1704,"children":1705},{},[1706,1711,1716],{"type":27,"tag":75,"props":1707,"children":1708},{},[1709],{"type":32,"value":1710},"同一个名字在多个地方被重复定义。",{"type":27,"tag":75,"props":1712,"children":1713},{},[1714],{"type":32,"value":1715},"payload 字段逐步追加，但没有人通知所有订阅者。",{"type":27,"tag":75,"props":1717,"children":1718},{},[1719],{"type":32,"value":1720},"少数 handler 开始自己断言 payload 类型，绕过总线约束。",{"type":27,"tag":28,"props":1722,"children":1723},{},[1724],{"type":32,"value":1725},"问题不在总线 API，而在契约没有集中。只要 event map 不是系统级事实，而是多个文件分散声明，漂移迟早会发生。",{"type":27,"tag":49,"props":1727,"children":1729},{"id":1728},"事件版本演进要像-api-一样认真",[1730],{"type":32,"value":1731},"事件版本演进要像 API 一样认真",{"type":27,"tag":28,"props":1733,"children":1734},{},[1735],{"type":32,"value":1736},"很多团队对 HTTP API 很谨慎，却对事件变更很随意。实际上，事件一旦被多个消费者订阅，它就是另一种 API。比较值得固定的规则包括：",{"type":27,"tag":71,"props":1738,"children":1739},{},[1740,1745,1750,1755],{"type":27,"tag":75,"props":1741,"children":1742},{},[1743],{"type":32,"value":1744},"payload 新增字段通常问题不大，但删除和重命名要视作破坏式变更。",{"type":27,"tag":75,"props":1746,"children":1747},{},[1748],{"type":32,"value":1749},"需要重大语义变化时，优先引入新事件名，而不是偷偷改旧 payload。",{"type":27,"tag":75,"props":1751,"children":1752},{},[1753],{"type":32,"value":1754},"公共事件和内部事件分层，不要让局部实现细节暴露给全局订阅方。",{"type":27,"tag":75,"props":1756,"children":1757},{},[1758],{"type":32,"value":1759},"如果事件跨进程或跨服务传播，运行时 schema 校验也要补上。",{"type":27,"tag":28,"props":1761,"children":1762},{},[1763],{"type":32,"value":1764},"事件不是“消息发出去就算完”，而是长期协作契约的一部分。",{"type":27,"tag":49,"props":1766,"children":1768},{"id":1767},"一份事件系统建模检查表",[1769],{"type":32,"value":1767},{"type":27,"tag":71,"props":1771,"children":1772},{},[1773,1778,1783,1788,1793],{"type":27,"tag":75,"props":1774,"children":1775},{},[1776],{"type":32,"value":1777},"事件名和 payload 是否被一张中心化 event map 绑定。",{"type":27,"tag":75,"props":1779,"children":1780},{},[1781],{"type":32,"value":1782},"事件命名是否表达业务事实，而不是宽泛动作。",{"type":27,"tag":75,"props":1784,"children":1785},{},[1786],{"type":32,"value":1787},"payload 是否只承诺最小必要字段，而非把整个对象一股脑透出去。",{"type":27,"tag":75,"props":1789,"children":1790},{},[1791],{"type":32,"value":1792},"订阅方是否能仅凭事件名获得稳定的 payload 类型。",{"type":27,"tag":75,"props":1794,"children":1795},{},[1796],{"type":32,"value":1797},"破坏式变更是否按 API 升级一样被认真对待。",{"type":27,"tag":49,"props":1799,"children":1800},{"id":443},[1801],{"type":32,"value":443},{"type":27,"tag":28,"props":1803,"children":1804},{},[1805],{"type":32,"value":1806},"TypeScript 让事件系统最值得做的一件事，就是把“事件名和 payload 的关系”从口头约定变成编译期契约。只要 event map 足够集中、命名足够克制、版本演进足够明确，发布订阅就不会随着规模扩大而变成字符串驱动的隐性耦合。",{"type":27,"tag":28,"props":1808,"children":1809},{},[1810],{"type":32,"value":1811},"本批次专题导航：",{"type":27,"tag":71,"props":1813,"children":1814},{},[1815,1841,1864,1879],{"type":27,"tag":75,"props":1816,"children":1817},{},[1818,1820,1826,1828,1834,1835],{"type":32,"value":1819},"工程边界：",{"type":27,"tag":960,"props":1821,"children":1823},{"href":1822},"/topics/typescript/typescript-project-references-tsconfig-layering-incremental-build-boundaries",[1824],{"type":32,"value":1825},"TypeScript 项目引用与 tsconfig 分层",{"type":32,"value":1827},"、",{"type":27,"tag":960,"props":1829,"children":1831},{"href":1830},"/topics/typescript/typescript-monorepo-dependency-boundaries-path-alias-exports-cycles",[1832],{"type":32,"value":1833},"TypeScript Monorepo 依赖边界治理",{"type":32,"value":1827},{"type":27,"tag":960,"props":1836,"children":1838},{"href":1837},"/topics/typescript/typescript-typecheck-performance-optimization-large-repo-playbook",[1839],{"type":32,"value":1840},"TypeScript 类型检查性能优化",{"type":27,"tag":75,"props":1842,"children":1843},{},[1844,1846,1850,1851,1857,1858],{"type":32,"value":1845},"协议协作：",{"type":27,"tag":960,"props":1847,"children":1848},{"href":1365},[1849],{"type":32,"value":1368},{"type":32,"value":1827},{"type":27,"tag":960,"props":1852,"children":1854},{"href":1853},"/topics/typescript/typescript-runtime-validation-static-types-schema-error-modeling",[1855],{"type":32,"value":1856},"TypeScript 运行时校验与静态类型协作",{"type":32,"value":1827},{"type":27,"tag":960,"props":1859,"children":1861},{"href":1860},"/topics/typescript/typescript-openapi-contract-codegen-handwritten-types-versioning",[1862],{"type":32,"value":1863},"TypeScript 与 OpenAPI 契约协同",{"type":27,"tag":75,"props":1865,"children":1866},{},[1867,1869,1874,1875],{"type":32,"value":1868},"落地复用：",{"type":27,"tag":960,"props":1870,"children":1871},{"href":1019},[1872],{"type":32,"value":1873},"TypeScript 设计模式实战",{"type":32,"value":1827},{"type":27,"tag":960,"props":1876,"children":1877},{"href":1376},[1878],{"type":32,"value":1379},{"type":27,"tag":75,"props":1880,"children":1881},{},[1882,1884,1889,1890],{"type":32,"value":1883},"状态建模：",{"type":27,"tag":960,"props":1885,"children":1886},{"href":1445},[1887],{"type":32,"value":1888},"TypeScript 事件系统建模",{"type":32,"value":1827},{"type":27,"tag":960,"props":1891,"children":1892},{"href":1387},[1893],{"type":32,"value":1390},{"type":27,"tag":28,"props":1895,"children":1896},{},[1897],{"type":32,"value":1354},{"type":27,"tag":71,"props":1899,"children":1900},{},[1901,1910,1919],{"type":27,"tag":75,"props":1902,"children":1903},{},[1904,1906],{"type":32,"value":1905},"如果你还没把边界输入做成可信数据，先读 ",{"type":27,"tag":960,"props":1907,"children":1908},{"href":1853},[1909],{"type":32,"value":1856},{"type":27,"tag":75,"props":1911,"children":1912},{},[1913,1915],{"type":32,"value":1914},"若你要把事件流继续落到页面状态，再看 ",{"type":27,"tag":960,"props":1916,"children":1917},{"href":1387},[1918],{"type":32,"value":1390},{"type":27,"tag":75,"props":1920,"children":1921},{},[1922,1924],{"type":32,"value":1923},"如果你希望对外契约和事件契约一起收口，可读 ",{"type":27,"tag":960,"props":1925,"children":1926},{"href":1365},[1927],{"type":32,"value":1368},{"type":27,"tag":28,"props":1929,"children":1930},{},[1931],{"type":32,"value":1395},{"type":27,"tag":71,"props":1933,"children":1934},{},[1935,1944,1951,1958],{"type":27,"tag":75,"props":1936,"children":1937},{},[1938],{"type":27,"tag":960,"props":1939,"children":1941},{"href":1940},"/topics/typescript/typescript-template-literal-types-real-world-applications",[1942],{"type":32,"value":1943},"TypeScript 模板字面量类型在真实项目中的应用",{"type":27,"tag":75,"props":1945,"children":1946},{},[1947],{"type":27,"tag":960,"props":1948,"children":1949},{"href":1412},[1950],{"type":32,"value":1415},{"type":27,"tag":75,"props":1952,"children":1953},{},[1954],{"type":27,"tag":960,"props":1955,"children":1956},{"href":1853},[1957],{"type":32,"value":1856},{"type":27,"tag":75,"props":1959,"children":1960},{},[1961],{"type":27,"tag":960,"props":1962,"children":1964},{"href":1963},"/topics/realtime-applications-guide",[1965],{"type":32,"value":1966},"实时应用开发完全指南",{"title":7,"searchDepth":560,"depth":560,"links":1968},[1969,1970,1971,1972,1973,1974,1975,1976],{"id":1504,"depth":563,"text":1507},{"id":1542,"depth":563,"text":1545},{"id":1581,"depth":563,"text":1584},{"id":1640,"depth":563,"text":1643},{"id":1679,"depth":563,"text":1682},{"id":1728,"depth":563,"text":1731},{"id":1767,"depth":563,"text":1767},{"id":443,"depth":563,"text":443},"content:topics:typescript:typescript-event-map-payload-contract-subscription-safety.md","topics/typescript/typescript-event-map-payload-contract-subscription-safety.md","topics/typescript/typescript-event-map-payload-contract-subscription-safety",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":1981,"image":18,"imageQuery":19,"pexelsPhotoId":20,"pexelsUrl":21,"featured":6,"readingTime":22,"body":1982,"_type":575,"_id":576,"_source":577,"_file":578,"_stem":579,"_extension":580},[13,14,15,16,17],{"type":24,"children":1983,"toc":2407},[1984,1994,1998,2002,2010,2014,2041,2045,2049,2057,2061,2069,2085,2089,2093,2101,2111,2119,2147,2151,2166,2170,2174,2182,2186,2190,2194,2202,2206,2221,2243,2247,2251,2258,2262,2266,2274,2278,2282,2290,2294,2298,2306,2310,2314,2403],{"type":27,"tag":28,"props":1985,"children":1986},{},[1987,1988,1993],{"type":32,"value":33},{"type":27,"tag":35,"props":1989,"children":1991},{"className":1990},[],[1992],{"type":32,"value":40},{"type":32,"value":42},{"type":27,"tag":28,"props":1995,"children":1996},{},[1997],{"type":32,"value":47},{"type":27,"tag":49,"props":1999,"children":2000},{"id":51},[2001],{"type":32,"value":51},{"type":27,"tag":55,"props":2003,"children":2005},{"code":57,"language":5,"meta":7,"className":2004},[59],[2006],{"type":27,"tag":35,"props":2007,"children":2008},{"__ignoreMap":7},[2009],{"type":32,"value":57},{"type":27,"tag":28,"props":2011,"children":2012},{},[2013],{"type":32,"value":69},{"type":27,"tag":71,"props":2015,"children":2016},{},[2017,2025,2033],{"type":27,"tag":75,"props":2018,"children":2019},{},[2020,2024],{"type":27,"tag":79,"props":2021,"children":2022},{},[2023],{"type":32,"value":14},{"type":32,"value":84},{"type":27,"tag":75,"props":2026,"children":2027},{},[2028,2032],{"type":27,"tag":79,"props":2029,"children":2030},{},[2031],{"type":32,"value":15},{"type":32,"value":93},{"type":27,"tag":75,"props":2034,"children":2035},{},[2036,2040],{"type":27,"tag":79,"props":2037,"children":2038},{},[2039],{"type":32,"value":16},{"type":32,"value":102},{"type":27,"tag":49,"props":2042,"children":2043},{"id":105},[2044],{"type":32,"value":108},{"type":27,"tag":28,"props":2046,"children":2047},{},[2048],{"type":32,"value":113},{"type":27,"tag":55,"props":2050,"children":2052},{"code":116,"language":5,"meta":7,"className":2051},[59],[2053],{"type":27,"tag":35,"props":2054,"children":2055},{"__ignoreMap":7},[2056],{"type":32,"value":116},{"type":27,"tag":28,"props":2058,"children":2059},{},[2060],{"type":32,"value":127},{"type":27,"tag":55,"props":2062,"children":2064},{"code":130,"language":5,"meta":7,"className":2063},[59],[2065],{"type":27,"tag":35,"props":2066,"children":2067},{"__ignoreMap":7},[2068],{"type":32,"value":130},{"type":27,"tag":28,"props":2070,"children":2071},{},[2072,2073,2078,2079,2084],{"type":32,"value":141},{"type":27,"tag":35,"props":2074,"children":2076},{"className":2075},[],[2077],{"type":32,"value":147},{"type":32,"value":149},{"type":27,"tag":35,"props":2080,"children":2082},{"className":2081},[],[2083],{"type":32,"value":155},{"type":32,"value":157},{"type":27,"tag":49,"props":2086,"children":2087},{"id":160},[2088],{"type":32,"value":163},{"type":27,"tag":28,"props":2090,"children":2091},{},[2092],{"type":32,"value":168},{"type":27,"tag":55,"props":2094,"children":2096},{"code":171,"language":5,"meta":7,"className":2095},[59],[2097],{"type":27,"tag":35,"props":2098,"children":2099},{"__ignoreMap":7},[2100],{"type":32,"value":171},{"type":27,"tag":28,"props":2102,"children":2103},{},[2104,2105,2110],{"type":32,"value":182},{"type":27,"tag":35,"props":2106,"children":2108},{"className":2107},[],[2109],{"type":32,"value":188},{"type":32,"value":190},{"type":27,"tag":55,"props":2112,"children":2114},{"code":193,"language":5,"meta":7,"className":2113},[59],[2115],{"type":27,"tag":35,"props":2116,"children":2117},{"__ignoreMap":7},[2118],{"type":32,"value":193},{"type":27,"tag":28,"props":2120,"children":2121},{},[2122,2123,2128,2129,2134,2135,2140,2141,2146],{"type":32,"value":204},{"type":27,"tag":35,"props":2124,"children":2126},{"className":2125},[],[2127],{"type":32,"value":210},{"type":32,"value":212},{"type":27,"tag":35,"props":2130,"children":2132},{"className":2131},[],[2133],{"type":32,"value":218},{"type":32,"value":220},{"type":27,"tag":35,"props":2136,"children":2138},{"className":2137},[],[2139],{"type":32,"value":226},{"type":32,"value":212},{"type":27,"tag":35,"props":2142,"children":2144},{"className":2143},[],[2145],{"type":32,"value":233},{"type":32,"value":235},{"type":27,"tag":28,"props":2148,"children":2149},{},[2150],{"type":32,"value":240},{"type":27,"tag":71,"props":2152,"children":2153},{},[2154,2158,2162],{"type":27,"tag":75,"props":2155,"children":2156},{},[2157],{"type":32,"value":248},{"type":27,"tag":75,"props":2159,"children":2160},{},[2161],{"type":32,"value":253},{"type":27,"tag":75,"props":2163,"children":2164},{},[2165],{"type":32,"value":258},{"type":27,"tag":49,"props":2167,"children":2168},{"id":261},[2169],{"type":32,"value":264},{"type":27,"tag":28,"props":2171,"children":2172},{},[2173],{"type":32,"value":269},{"type":27,"tag":55,"props":2175,"children":2177},{"code":272,"language":5,"meta":7,"className":2176},[59],[2178],{"type":27,"tag":35,"props":2179,"children":2180},{"__ignoreMap":7},[2181],{"type":32,"value":272},{"type":27,"tag":28,"props":2183,"children":2184},{},[2185],{"type":32,"value":283},{"type":27,"tag":49,"props":2187,"children":2188},{"id":286},[2189],{"type":32,"value":289},{"type":27,"tag":28,"props":2191,"children":2192},{},[2193],{"type":32,"value":294},{"type":27,"tag":55,"props":2195,"children":2197},{"code":297,"language":5,"meta":7,"className":2196},[59],[2198],{"type":27,"tag":35,"props":2199,"children":2200},{"__ignoreMap":7},[2201],{"type":32,"value":297},{"type":27,"tag":28,"props":2203,"children":2204},{},[2205],{"type":32,"value":308},{"type":27,"tag":310,"props":2207,"children":2208},{},[2209,2213,2217],{"type":27,"tag":75,"props":2210,"children":2211},{},[2212],{"type":32,"value":317},{"type":27,"tag":75,"props":2214,"children":2215},{},[2216],{"type":32,"value":322},{"type":27,"tag":75,"props":2218,"children":2219},{},[2220],{"type":32,"value":327},{"type":27,"tag":28,"props":2222,"children":2223},{},[2224,2225,2230,2231,2236,2237,2242],{"type":32,"value":332},{"type":27,"tag":35,"props":2226,"children":2228},{"className":2227},[],[2229],{"type":32,"value":338},{"type":32,"value":340},{"type":27,"tag":35,"props":2232,"children":2234},{"className":2233},[],[2235],{"type":32,"value":346},{"type":32,"value":348},{"type":27,"tag":35,"props":2238,"children":2240},{"className":2239},[],[2241],{"type":32,"value":354},{"type":32,"value":356},{"type":27,"tag":49,"props":2244,"children":2245},{"id":359},[2246],{"type":32,"value":359},{"type":27,"tag":28,"props":2248,"children":2249},{},[2250],{"type":32,"value":366},{"type":27,"tag":55,"props":2252,"children":2253},{"code":369},[2254],{"type":27,"tag":35,"props":2255,"children":2256},{"__ignoreMap":7},[2257],{"type":32,"value":369},{"type":27,"tag":49,"props":2259,"children":2260},{"id":377},[2261],{"type":32,"value":377},{"type":27,"tag":381,"props":2263,"children":2264},{"id":383},[2265],{"type":32,"value":386},{"type":27,"tag":55,"props":2267,"children":2269},{"code":389,"language":5,"meta":7,"className":2268},[59],[2270],{"type":27,"tag":35,"props":2271,"children":2272},{"__ignoreMap":7},[2273],{"type":32,"value":389},{"type":27,"tag":28,"props":2275,"children":2276},{},[2277],{"type":32,"value":400},{"type":27,"tag":381,"props":2279,"children":2280},{"id":403},[2281],{"type":32,"value":406},{"type":27,"tag":55,"props":2283,"children":2285},{"code":409,"language":5,"meta":7,"className":2284},[59],[2286],{"type":27,"tag":35,"props":2287,"children":2288},{"__ignoreMap":7},[2289],{"type":32,"value":409},{"type":27,"tag":28,"props":2291,"children":2292},{},[2293],{"type":32,"value":420},{"type":27,"tag":381,"props":2295,"children":2296},{"id":423},[2297],{"type":32,"value":426},{"type":27,"tag":55,"props":2299,"children":2301},{"code":429,"language":5,"meta":7,"className":2300},[59],[2302],{"type":27,"tag":35,"props":2303,"children":2304},{"__ignoreMap":7},[2305],{"type":32,"value":429},{"type":27,"tag":28,"props":2307,"children":2308},{},[2309],{"type":32,"value":440},{"type":27,"tag":49,"props":2311,"children":2312},{"id":443},[2313],{"type":32,"value":443},{"type":27,"tag":447,"props":2315,"children":2316},{},[2317,2335],{"type":27,"tag":451,"props":2318,"children":2319},{},[2320],{"type":27,"tag":455,"props":2321,"children":2322},{},[2323,2327,2331],{"type":27,"tag":459,"props":2324,"children":2325},{},[2326],{"type":32,"value":463},{"type":27,"tag":459,"props":2328,"children":2329},{},[2330],{"type":32,"value":468},{"type":27,"tag":459,"props":2332,"children":2333},{},[2334],{"type":32,"value":473},{"type":27,"tag":475,"props":2336,"children":2337},{},[2338,2358,2373,2388],{"type":27,"tag":455,"props":2339,"children":2340},{},[2341,2345,2354],{"type":27,"tag":482,"props":2342,"children":2343},{},[2344],{"type":32,"value":486},{"type":27,"tag":482,"props":2346,"children":2347},{},[2348,2349],{"type":32,"value":491},{"type":27,"tag":35,"props":2350,"children":2352},{"className":2351},[],[2353],{"type":32,"value":40},{"type":27,"tag":482,"props":2355,"children":2356},{},[2357],{"type":32,"value":501},{"type":27,"tag":455,"props":2359,"children":2360},{},[2361,2365,2369],{"type":27,"tag":482,"props":2362,"children":2363},{},[2364],{"type":32,"value":509},{"type":27,"tag":482,"props":2366,"children":2367},{},[2368],{"type":32,"value":15},{"type":27,"tag":482,"props":2370,"children":2371},{},[2372],{"type":32,"value":518},{"type":27,"tag":455,"props":2374,"children":2375},{},[2376,2380,2384],{"type":27,"tag":482,"props":2377,"children":2378},{},[2379],{"type":32,"value":526},{"type":27,"tag":482,"props":2381,"children":2382},{},[2383],{"type":32,"value":16},{"type":27,"tag":482,"props":2385,"children":2386},{},[2387],{"type":32,"value":535},{"type":27,"tag":455,"props":2389,"children":2390},{},[2391,2395,2399],{"type":27,"tag":482,"props":2392,"children":2393},{},[2394],{"type":32,"value":543},{"type":27,"tag":482,"props":2396,"children":2397},{},[2398],{"type":32,"value":548},{"type":27,"tag":482,"props":2400,"children":2401},{},[2402],{"type":32,"value":553},{"type":27,"tag":28,"props":2404,"children":2405},{},[2406],{"type":32,"value":558},{"title":7,"searchDepth":560,"depth":560,"links":2408},[2409,2410,2411,2412,2413,2414,2415,2420],{"id":51,"depth":563,"text":51},{"id":105,"depth":563,"text":108},{"id":160,"depth":563,"text":163},{"id":261,"depth":563,"text":264},{"id":286,"depth":563,"text":289},{"id":359,"depth":563,"text":359},{"id":377,"depth":563,"text":377,"children":2416},[2417,2418,2419],{"id":383,"depth":560,"text":386},{"id":403,"depth":560,"text":406},{"id":423,"depth":560,"text":426},{"id":443,"depth":563,"text":443},1782088273909]