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