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