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