[{"data":1,"prerenderedAt":3102},["ShallowReactive",2],{"article-/topics/typescript/typescript-js-migration-incremental-strict-mode-playbook":3,"related-typescript":825,"content-query-lZO80qJKK7":2469},{"_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,1951],{"_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":10,"topic":5,"author":11,"tags":1267,"image":1273,"imageQuery":1274,"pexelsPhotoId":1275,"pexelsUrl":1276,"featured":6,"readingTime":1277,"body":1278,"_type":819,"_id":1948,"_source":821,"_file":1949,"_stem":1950,"_extension":824},"/topics/typescript/typescript-conditional-types-infer-pattern-matching","TypeScript 条件类型与 infer：从模式匹配到递归类型推导的实战路径","条件类型和 infer 是 TypeScript 类型系统中最灵活也最容易被误用的机制。本文从实际代码场景出发，讲清条件类型的匹配规则、infer 的推断时机、递归类型推导的边界，以及它们如何帮助写出更精确的工具类型。",[13,1268,1269,1270,1271,1272],"条件类型","infer","类型推导","递归类型","高级类型","/images/articles/typescript-conditional-types-infer-pattern-matching-featured.jpg","typescript code conditional type programming laptop screen",34803986,"https://www.pexels.com/photo/modern-laptop-displaying-code-in-cozy-workspace-34803986/",19,{"type":24,"children":1279,"toc":1935},[1280,1316,1321,1327,1355,1364,1369,1378,1399,1411,1420,1425,1434,1440,1450,1455,1464,1469,1545,1557,1566,1583,1589,1594,1603,1608,1617,1623,1632,1638,1647,1660,1666,1675,1681,1686,1695,1700,1728,1734,1739,1748,1753,1762,1767,1772,1777,1786,1791,1797,1930],{"type":27,"tag":28,"props":1281,"children":1282},{},[1283,1285,1290,1292,1298,1300,1306,1308,1314],{"type":32,"value":1284},"条件类型和 ",{"type":27,"tag":58,"props":1286,"children":1288},{"className":1287},[],[1289],{"type":32,"value":1269},{"type":32,"value":1291}," 是 TypeScript 类型系统里最强大的两个机制，也是很多人文档看完但不会用的两个概念。文档告诉你 ",{"type":27,"tag":58,"props":1293,"children":1295},{"className":1294},[],[1296],{"type":32,"value":1297},"T extends U ? X : Y",{"type":32,"value":1299}," 是条件类型，",{"type":27,"tag":58,"props":1301,"children":1303},{"className":1302},[],[1304],{"type":32,"value":1305},"infer R",{"type":32,"value":1307}," 可以提取类型变量，但实际看到 ",{"type":27,"tag":58,"props":1309,"children":1311},{"className":1310},[],[1312],{"type":32,"value":1313},"ReturnType\u003CT>",{"type":32,"value":1315}," 的源码时还是懵的。",{"type":27,"tag":28,"props":1317,"children":1318},{},[1319],{"type":32,"value":1320},"这不是理解能力的问题。条件类型的难点不在语法，而在它运行在一个和值空间完全不同的逻辑系统里。类型不是数据，不能计算，不存在运行时流程控制。条件类型的\"条件\"本质上是类型结构的模式匹配——它不看取值范围，只看结构形状。",{"type":27,"tag":35,"props":1322,"children":1324},{"id":1323},"条件类型的匹配规则结构兼容不是完全相等",[1325],{"type":32,"value":1326},"条件类型的匹配规则：结构兼容，不是完全相等",{"type":27,"tag":28,"props":1328,"children":1329},{},[1330,1332,1338,1340,1346,1348,1353],{"type":32,"value":1331},"条件类型 ",{"type":27,"tag":58,"props":1333,"children":1335},{"className":1334},[],[1336],{"type":32,"value":1337},"T extends U ? A : B",{"type":32,"value":1339}," 的判断标准，和函数签名里的 ",{"type":27,"tag":58,"props":1341,"children":1343},{"className":1342},[],[1344],{"type":32,"value":1345},"extends",{"type":32,"value":1347}," 约束其实是同一套规则：",{"type":27,"tag":743,"props":1349,"children":1350},{},[1351],{"type":32,"value":1352},"U 能不能收容 T",{"type":32,"value":1354},"。不是 T 和 U 长得一样，而是 T 的类型的值能不能赋值给 U。",{"type":27,"tag":125,"props":1356,"children":1359},{"className":1357,"code":1358,"language":5,"meta":7},[219],"type IsString\u003CT> = T extends string ? true : false\n\ntype R1 = IsString\u003C'hello'>  // true —— 字面量 'hello' 可以赋值给 string\ntype R2 = IsString\u003C42>       // false —— number 不能赋值给 string\ntype R3 = IsString\u003Cstring>   // true —— string extends string\n",[1360],{"type":27,"tag":58,"props":1361,"children":1362},{"__ignoreMap":7},[1363],{"type":32,"value":1358},{"type":27,"tag":28,"props":1365,"children":1366},{},[1367],{"type":32,"value":1368},"看起来简单，但换成对象类型就很容易判断错：",{"type":27,"tag":125,"props":1370,"children":1373},{"className":1371,"code":1372,"language":5,"meta":7},[219],"type HasName\u003CT> = T extends { name: string } ? 'yes' : 'no'\n\ntype R4 = HasName\u003C{ name: 'alice' }>          // 'yes' —— 含 name 且兼容\ntype R5 = HasName\u003C{ name: string; age: number }>  // 'yes' —— 多字段不影响\ntype R6 = HasName\u003C{ age: number }>             // 'no' —— 缺 name\n",[1374],{"type":27,"tag":58,"props":1375,"children":1376},{"__ignoreMap":7},[1377],{"type":32,"value":1372},{"type":27,"tag":28,"props":1379,"children":1380},{},[1381,1383,1389,1391,1397],{"type":32,"value":1382},"这里经常有人困惑：为什么 ",{"type":27,"tag":58,"props":1384,"children":1386},{"className":1385},[],[1387],{"type":32,"value":1388},"{ name: string; age: number }",{"type":32,"value":1390}," 能匹配 ",{"type":27,"tag":58,"props":1392,"children":1394},{"className":1393},[],[1395],{"type":32,"value":1396},"{ name: string }",{"type":32,"value":1398},"？因为条件类型看的是 U（右侧）能不能收容 T（左侧），不是反过来。对象类型的兼容性是结构子类型——多字段的结构可以被少字段的结构收容，只要被检查的字段满足要求。",{"type":27,"tag":28,"props":1400,"children":1401},{},[1402,1404,1409],{"type":32,"value":1403},"把这个规则反过来，就是 ",{"type":27,"tag":58,"props":1405,"children":1407},{"className":1406},[],[1408],{"type":32,"value":1345},{"type":32,"value":1410}," 约束里的分配条件类型（distributive conditional types）：",{"type":27,"tag":125,"props":1412,"children":1415},{"className":1413,"code":1414,"language":5,"meta":7},[219],"type ToArray\u003CT> = T extends unknown ? T[] : never\n\ntype R7 = ToArray\u003Cstring | number>\n// string[] | number[] —— 不是 (string | number)[]\n",[1416],{"type":27,"tag":58,"props":1417,"children":1418},{"__ignoreMap":7},[1419],{"type":32,"value":1414},{"type":27,"tag":28,"props":1421,"children":1422},{},[1423],{"type":32,"value":1424},"联合类型会被拆开分别匹配再合并，这就是分配律。关掉分配律的方法是用方括号包裹泛型参数：",{"type":27,"tag":125,"props":1426,"children":1429},{"className":1427,"code":1428,"language":5,"meta":7},[219],"type ToArrayNonDist\u003CT> = [T] extends [unknown] ? T[] : never\n\ntype R8 = ToArrayNonDist\u003Cstring | number>\n// (string | number)[]\n",[1430],{"type":27,"tag":58,"props":1431,"children":1432},{"__ignoreMap":7},[1433],{"type":32,"value":1428},{"type":27,"tag":35,"props":1435,"children":1437},{"id":1436},"infer-的实质在匹配位置声明类型变量",[1438],{"type":32,"value":1439},"infer 的实质：在匹配位置声明类型变量",{"type":27,"tag":28,"props":1441,"children":1442},{},[1443,1448],{"type":27,"tag":58,"props":1444,"children":1446},{"className":1445},[],[1447],{"type":32,"value":1269},{"type":32,"value":1449}," 不是什么黑魔法——它只是在条件类型的 extends 子句中声明一个待推导的类型变量，然后让 TypeScript 在匹配过程中自动填充它。",{"type":27,"tag":28,"props":1451,"children":1452},{},[1453],{"type":32,"value":1454},"最简单的例子是从函数类型提取返回值类型：",{"type":27,"tag":125,"props":1456,"children":1459},{"className":1457,"code":1458,"language":5,"meta":7},[219],"type MyReturnType\u003CT> = T extends (...args: any[]) => infer R ? R : never\n\ntype Fn = (x: number, y: string) => boolean\ntype R9 = MyReturnType\u003CFn>  // boolean\n",[1460],{"type":27,"tag":58,"props":1461,"children":1462},{"__ignoreMap":7},[1463],{"type":32,"value":1458},{"type":27,"tag":28,"props":1465,"children":1466},{},[1467],{"type":32,"value":1468},"流程是这样的：",{"type":27,"tag":735,"props":1470,"children":1471},{},[1472,1483,1494,1519,1535],{"type":27,"tag":739,"props":1473,"children":1474},{},[1475,1477],{"type":32,"value":1476},"传入 ",{"type":27,"tag":58,"props":1478,"children":1480},{"className":1479},[],[1481],{"type":32,"value":1482},"(x: number, y: string) => boolean",{"type":27,"tag":739,"props":1484,"children":1485},{},[1486,1488],{"type":32,"value":1487},"检查 ",{"type":27,"tag":58,"props":1489,"children":1491},{"className":1490},[],[1492],{"type":32,"value":1493},"(x: number, y: string) => boolean extends (...args: any[]) => infer R",{"type":27,"tag":739,"props":1495,"children":1496},{},[1497,1503,1505,1511,1513],{"type":27,"tag":58,"props":1498,"children":1500},{"className":1499},[],[1501],{"type":32,"value":1502},"(...args: any[])",{"type":32,"value":1504}," 匹配 ",{"type":27,"tag":58,"props":1506,"children":1508},{"className":1507},[],[1509],{"type":32,"value":1510},"(x: number, y: string)",{"type":32,"value":1512},"，剩余 ",{"type":27,"tag":58,"props":1514,"children":1516},{"className":1515},[],[1517],{"type":32,"value":1518},"=> boolean",{"type":27,"tag":739,"props":1520,"children":1521},{},[1522,1527,1529],{"type":27,"tag":58,"props":1523,"children":1525},{"className":1524},[],[1526],{"type":32,"value":1305},{"type":32,"value":1528}," 被推导为 ",{"type":27,"tag":58,"props":1530,"children":1532},{"className":1531},[],[1533],{"type":32,"value":1534},"boolean",{"type":27,"tag":739,"props":1536,"children":1537},{},[1538,1540],{"type":32,"value":1539},"返回 ",{"type":27,"tag":58,"props":1541,"children":1543},{"className":1542},[],[1544],{"type":32,"value":1534},{"type":27,"tag":28,"props":1546,"children":1547},{},[1548,1550,1555],{"type":32,"value":1549},"但 ",{"type":27,"tag":58,"props":1551,"children":1553},{"className":1552},[],[1554],{"type":32,"value":1269},{"type":32,"value":1556}," 有一个容易踩的坑——它只在条件类型为 true 的分支里可用：",{"type":27,"tag":125,"props":1558,"children":1561},{"className":1559,"code":1560,"language":5,"meta":7},[219],"// 错误用法\ntype ExtractPromiseBad\u003CT> = T extends Promise\u003Cinfer U> ? U : never\ntype X = ExtractPromiseBad\u003CPromise\u003Cstring>>  // string —— 正确\n\n// 但不代表 infer 可以出现在任意位置\ntype Wrong\u003CT> = infer U extends T ? U : never  // 语法错误\n",[1562],{"type":27,"tag":58,"props":1563,"children":1564},{"__ignoreMap":7},[1565],{"type":32,"value":1560},{"type":27,"tag":28,"props":1567,"children":1568},{},[1569,1574,1576,1581],{"type":27,"tag":58,"props":1570,"children":1572},{"className":1571},[],[1573],{"type":32,"value":1269},{"type":32,"value":1575}," 必须出现在 ",{"type":27,"tag":58,"props":1577,"children":1579},{"className":1578},[],[1580],{"type":32,"value":1345},{"type":32,"value":1582}," 子句的右侧，而且只能在条件为 true 时才能被使用。",{"type":27,"tag":35,"props":1584,"children":1586},{"id":1585},"递归类型推导条件类型-infer-的组合",[1587],{"type":32,"value":1588},"递归类型推导：条件类型 + infer 的组合",{"type":27,"tag":28,"props":1590,"children":1591},{},[1592],{"type":32,"value":1593},"真正体现 infer 威力的是递归类型推导。考虑一个场景：从嵌套对象中提取所有叶子路径的值的类型。",{"type":27,"tag":125,"props":1595,"children":1598},{"className":1596,"code":1597,"language":5,"meta":7},[219],"type DeepValue\u003CT> = T extends Record\u003Cstring, infer V>\n  ? V extends Record\u003Cstring, any>\n    ? DeepValue\u003CV>\n    : V\n  : T\n\ntype Obj = { a: { b: { c: string } } }\ntype R10 = DeepValue\u003CObj>  // string\n",[1599],{"type":27,"tag":58,"props":1600,"children":1601},{"__ignoreMap":7},[1602],{"type":32,"value":1597},{"type":27,"tag":28,"props":1604,"children":1605},{},[1606],{"type":32,"value":1607},"递归类型推导需要特别注意终止条件。如果没有终止条件，TypeScript 会达到递归深度上限（通常是 50 层）然后报错：",{"type":27,"tag":125,"props":1609,"children":1612},{"className":1610,"code":1611,"language":5,"meta":7},[219],"type InfiniteDeep\u003CT> = T extends Record\u003Cstring, infer V>\n  ? InfiniteDeep\u003CV>\n  : never\n// 编译器可能报 \"Type instantiation is excessively deep and possibly infinite\"\n",[1613],{"type":27,"tag":58,"props":1614,"children":1615},{"__ignoreMap":7},[1616],{"type":32,"value":1611},{"type":27,"tag":46,"props":1618,"children":1620},{"id":1619},"实用模式-1展开-promise",[1621],{"type":32,"value":1622},"实用模式 1：展开 Promise",{"type":27,"tag":125,"props":1624,"children":1627},{"className":1625,"code":1626,"language":5,"meta":7},[219],"type Unwrap\u003CT> = T extends Promise\u003Cinfer U> ? Unwrap\u003CU> : T\n\ntype R11 = Unwrap\u003CPromise\u003CPromise\u003CPromise\u003Cstring>>>>\n// string —— 递归展开所有层级\n",[1628],{"type":27,"tag":58,"props":1629,"children":1630},{"__ignoreMap":7},[1631],{"type":32,"value":1626},{"type":27,"tag":46,"props":1633,"children":1635},{"id":1634},"实用模式-2提取函数参数类型",[1636],{"type":32,"value":1637},"实用模式 2：提取函数参数类型",{"type":27,"tag":125,"props":1639,"children":1642},{"className":1640,"code":1641,"language":5,"meta":7},[219],"type MyParameters\u003CT> = T extends (...args: infer P) => any ? P : never\n\ntype Fn2 = (a: number, b: string, c: boolean) => void\ntype Params = MyParameters\u003CFn2>\n// [number, string, boolean] —— 元组类型\n",[1643],{"type":27,"tag":58,"props":1644,"children":1645},{"__ignoreMap":7},[1646],{"type":32,"value":1641},{"type":27,"tag":28,"props":1648,"children":1649},{},[1650,1652,1658],{"type":32,"value":1651},"注意这里 infer P 不是单个类型，而是参数元组。TypeScript 会把 ",{"type":27,"tag":58,"props":1653,"children":1655},{"className":1654},[],[1656],{"type":32,"value":1657},"...args",{"type":32,"value":1659}," 的展开类型自动推断为元组。",{"type":27,"tag":46,"props":1661,"children":1663},{"id":1662},"实用模式-3条件推断-模板字面量",[1664],{"type":32,"value":1665},"实用模式 3：条件推断 + 模板字面量",{"type":27,"tag":125,"props":1667,"children":1670},{"className":1668,"code":1669,"language":5,"meta":7},[219],"type ExtractId\u003CT extends string> =\n  T extends `id-${infer Rest}` ? Rest : never\n\ntype R12 = ExtractId\u003C'id-abc123'>  // 'abc123'\ntype R13 = ExtractId\u003C'name-xyz'>   // never\n",[1671],{"type":27,"tag":58,"props":1672,"children":1673},{"__ignoreMap":7},[1674],{"type":32,"value":1669},{"type":27,"tag":35,"props":1676,"children":1678},{"id":1677},"实战场景实现一个类型安全的路径提取器",[1679],{"type":32,"value":1680},"实战场景：实现一个类型安全的路径提取器",{"type":27,"tag":28,"props":1682,"children":1683},{},[1684],{"type":32,"value":1685},"下面用一个稍微复杂一点的例子，展示条件类型和 infer 的组合威力。假设你需要一个类型，能从深层嵌套对象中提取指定路径的值类型：",{"type":27,"tag":125,"props":1687,"children":1690},{"className":1688,"code":1689,"language":5,"meta":7},[219],"type PathValue\u003CT, P extends string> =\n  P extends `${infer K}.${infer Rest}`\n    ? K extends keyof T\n      ? PathValue\u003CT[K], Rest>\n      : never\n    : P extends keyof T\n      ? T[P]\n      : never\n\ntype Data = {\n  user: {\n    profile: {\n      name: string\n      age: number\n    }\n    settings: {\n      theme: 'light' | 'dark'\n    }\n  }\n}\n\ntype R14 = PathValue\u003CData, 'user.profile.name'>  // string\ntype R15 = PathValue\u003CData, 'user.settings.theme'>  // 'light' | 'dark'\ntype R16 = PathValue\u003CData, 'user.profile.email'>  // never —— 路径不存在\n",[1691],{"type":27,"tag":58,"props":1692,"children":1693},{"__ignoreMap":7},[1694],{"type":32,"value":1689},{"type":27,"tag":28,"props":1696,"children":1697},{},[1698],{"type":32,"value":1699},"这个类型有两个递归分支：",{"type":27,"tag":735,"props":1701,"children":1702},{},[1703,1716],{"type":27,"tag":739,"props":1704,"children":1705},{},[1706,1708,1714],{"type":32,"value":1707},"如果路径包含 ",{"type":27,"tag":58,"props":1709,"children":1711},{"className":1710},[],[1712],{"type":32,"value":1713},".",{"type":32,"value":1715},"，用模板字面量拆分出第一段和剩余路径",{"type":27,"tag":739,"props":1717,"children":1718},{},[1719,1721,1726],{"type":32,"value":1720},"如果路径不包含 ",{"type":27,"tag":58,"props":1722,"children":1724},{"className":1723},[],[1725],{"type":32,"value":1713},{"type":32,"value":1727},"，直接取 key",{"type":27,"tag":35,"props":1729,"children":1731},{"id":1730},"infer-的逆协变与逆变",[1732],{"type":32,"value":1733},"infer 的逆协变与逆变",{"type":27,"tag":28,"props":1735,"children":1736},{},[1737],{"type":32,"value":1738},"当 infer 出现在不同位置时，它的赋值行为不同。这是很多人踩坑的地方：",{"type":27,"tag":125,"props":1740,"children":1743},{"className":1741,"code":1742,"language":5,"meta":7},[219],"// 协变位置（函数返回值）\ntype CoExtract\u003CT> = T extends () => infer R ? R : never\ntype R17 = CoExtract\u003C() => string | number>  // string | number\n\n// 逆变位置（函数参数）\ntype ContraExtract\u003CT> = T extends (x: infer P) => any ? P : never\ntype R18 = ContraExtract\u003C(x: string | number) => void>\n// string | number —— 看起来一样\n",[1744],{"type":27,"tag":58,"props":1745,"children":1746},{"__ignoreMap":7},[1747],{"type":32,"value":1742},{"type":27,"tag":28,"props":1749,"children":1750},{},[1751],{"type":32,"value":1752},"但在泛型中，逆变位置的 infer 会有不同的行为：",{"type":27,"tag":125,"props":1754,"children":1757},{"className":1755,"code":1756,"language":5,"meta":7},[219],"type UnionToIntersection\u003CT> =\n  (T extends any ? (x: T) => void : never) extends (x: infer R) => void\n    ? R\n    : never\n\ntype R19 = UnionToIntersection\u003Cstring | number>\n// string & number —— 联合转交叉的经典实现\n",[1758],{"type":27,"tag":58,"props":1759,"children":1760},{"__ignoreMap":7},[1761],{"type":32,"value":1756},{"type":27,"tag":28,"props":1763,"children":1764},{},[1765],{"type":32,"value":1766},"这个技巧利用了函数参数的逆变位置：当多个类型分布在同一个逆变位置上时，TypeScript 会尝试取交集。",{"type":27,"tag":35,"props":1768,"children":1770},{"id":1769},"递归类型推导的性能问题",[1771],{"type":32,"value":1769},{"type":27,"tag":28,"props":1773,"children":1774},{},[1775],{"type":32,"value":1776},"递归条件类型不是免费的。每层递归都会增加类型实例化的深度。以下场景尤其需要注意：",{"type":27,"tag":125,"props":1778,"children":1781},{"className":1779,"code":1780,"language":5,"meta":7},[219],"// 逐个元素处理的递归（性能较差的写法）\ntype DeepReadonlyArray\u003CT extends any[]> = {\n  [K in keyof T]: T[K] extends object\n    ? DeepReadonlyArray\u003CT[K]>\n    : T[K]\n}\n\n// 用映射类型减少递归深度（优化写法）\ntype DeepReadonly\u003CT> = {\n  readonly [K in keyof T]: T[K] extends Record\u003Cstring, any>\n    ? DeepReadonly\u003CT[K]>\n    : T[K]\n}\n",[1782],{"type":27,"tag":58,"props":1783,"children":1784},{"__ignoreMap":7},[1785],{"type":32,"value":1780},{"type":27,"tag":28,"props":1787,"children":1788},{},[1789],{"type":32,"value":1790},"映射类型的单层操作不会增加递归深度，而递归条件类型每展开一层都计数。对于可能超过 30 层嵌套的结构，先评估是否真的需要这么深的递归。",{"type":27,"tag":35,"props":1792,"children":1794},{"id":1793},"总结条件类型-infer-的使用建议",[1795],{"type":32,"value":1796},"总结：条件类型 + infer 的使用建议",{"type":27,"tag":549,"props":1798,"children":1799},{},[1800,1821],{"type":27,"tag":553,"props":1801,"children":1802},{},[1803],{"type":27,"tag":557,"props":1804,"children":1805},{},[1806,1811,1816],{"type":27,"tag":561,"props":1807,"children":1808},{},[1809],{"type":32,"value":1810},"场景",{"type":27,"tag":561,"props":1812,"children":1813},{},[1814],{"type":32,"value":1815},"使用方式",{"type":27,"tag":561,"props":1817,"children":1818},{},[1819],{"type":32,"value":1820},"注意事项",{"type":27,"tag":587,"props":1822,"children":1823},{},[1824,1846,1868,1894,1912],{"type":27,"tag":557,"props":1825,"children":1826},{},[1827,1832,1841],{"type":27,"tag":594,"props":1828,"children":1829},{},[1830],{"type":32,"value":1831},"提取函数返回值类型",{"type":27,"tag":594,"props":1833,"children":1834},{},[1835],{"type":27,"tag":58,"props":1836,"children":1838},{"className":1837},[],[1839],{"type":32,"value":1840},"(...args: any[]) => infer R",{"type":27,"tag":594,"props":1842,"children":1843},{},[1844],{"type":32,"value":1845},"简单直接，无限参数兼容",{"type":27,"tag":557,"props":1847,"children":1848},{},[1849,1854,1863],{"type":27,"tag":594,"props":1850,"children":1851},{},[1852],{"type":32,"value":1853},"提取 Promise 内部类型",{"type":27,"tag":594,"props":1855,"children":1856},{},[1857],{"type":27,"tag":58,"props":1858,"children":1860},{"className":1859},[],[1861],{"type":32,"value":1862},"Promise\u003Cinfer T>",{"type":27,"tag":594,"props":1864,"children":1865},{},[1866],{"type":32,"value":1867},"递归展开需加终止条件",{"type":27,"tag":557,"props":1869,"children":1870},{},[1871,1876,1881],{"type":27,"tag":594,"props":1872,"children":1873},{},[1874],{"type":32,"value":1875},"路径拆分",{"type":27,"tag":594,"props":1877,"children":1878},{},[1879],{"type":32,"value":1880},"模板字面量 + infer",{"type":27,"tag":594,"props":1882,"children":1883},{},[1884,1886,1892],{"type":32,"value":1885},"注意 ",{"type":27,"tag":58,"props":1887,"children":1889},{"className":1888},[],[1890],{"type":32,"value":1891},"keyof",{"type":32,"value":1893}," 配合",{"type":27,"tag":557,"props":1895,"children":1896},{},[1897,1902,1907],{"type":27,"tag":594,"props":1898,"children":1899},{},[1900],{"type":32,"value":1901},"联合转交叉",{"type":27,"tag":594,"props":1903,"children":1904},{},[1905],{"type":32,"value":1906},"逆变位置 infer",{"type":27,"tag":594,"props":1908,"children":1909},{},[1910],{"type":32,"value":1911},"理解原理再使用",{"type":27,"tag":557,"props":1913,"children":1914},{},[1915,1920,1925],{"type":27,"tag":594,"props":1916,"children":1917},{},[1918],{"type":32,"value":1919},"递归对象类型",{"type":27,"tag":594,"props":1921,"children":1922},{},[1923],{"type":32,"value":1924},"映射类型 + 条件",{"type":27,"tag":594,"props":1926,"children":1927},{},[1928],{"type":32,"value":1929},"评估深度，避免爆炸",{"type":27,"tag":28,"props":1931,"children":1932},{},[1933],{"type":32,"value":1934},"条件类型的难点从来不在语法，而在于它遵循的是结构子类型规则，不是等值规则。写条件类型时，心里应该想着\"这个形状能不能匹配那个形状\"，而不是\"这两个值相不相等\"。",{"title":7,"searchDepth":796,"depth":796,"links":1936},[1937,1938,1939,1944,1945,1946,1947],{"id":1323,"depth":799,"text":1326},{"id":1436,"depth":799,"text":1439},{"id":1585,"depth":799,"text":1588,"children":1940},[1941,1942,1943],{"id":1619,"depth":796,"text":1622},{"id":1634,"depth":796,"text":1637},{"id":1662,"depth":796,"text":1665},{"id":1677,"depth":799,"text":1680},{"id":1730,"depth":799,"text":1733},{"id":1769,"depth":799,"text":1769},{"id":1793,"depth":799,"text":1796},"content:topics:typescript:typescript-conditional-types-infer-pattern-matching.md","topics/typescript/typescript-conditional-types-infer-pattern-matching.md","topics/typescript/typescript-conditional-types-infer-pattern-matching",{"_path":1952,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1953,"description":1954,"date":10,"topic":5,"author":11,"tags":1955,"image":1961,"imageQuery":1962,"pexelsPhotoId":1963,"pexelsUrl":1964,"featured":6,"readingTime":1965,"body":1966,"_type":819,"_id":2466,"_source":821,"_file":2467,"_stem":2468,"_extension":824},"/topics/typescript/typescript-decorators-real-world-validation-logging-di","TypeScript 装饰器的实际使用场景","TypeScript 装饰器在经历了多次提案变更后，终于在 5.0 版本支持了 ECMAScript 标准的装饰器。本文对比新旧装饰器语法，讲解类装饰器、方法装饰器、访问器装饰器和属性装饰器的实际用途，并用四种模式展示它们在项目中的应用。",[13,1956,1957,1958,1959,1960],"装饰器","依赖注入","日志","校验","AOP","/images/articles/typescript-decorators-real-world-validation-logging-di-featured.jpg","typescript decorator pattern code programming laptop",34804022,"https://www.pexels.com/photo/colorful-code-on-screen-with-light-patterns-34804022/",16,{"type":24,"children":1967,"toc":2457},[1968,1989,1994,1999,2004,2013,2033,2039,2044,2053,2058,2066,2071,2077,2082,2091,2096,2102,2107,2116,2122,2127,2136,2149,2155,2160,2293,2298,2307,2349,2353,2452],{"type":27,"tag":28,"props":1969,"children":1970},{},[1971,1973,1979,1981,1987],{"type":32,"value":1972},"TypeScript 装饰器经历了一段不太平滑的演进。早期版本（5.0 之前）使用的是实验性的\"传统装饰器\"（experimental decorators），需要在 ",{"type":27,"tag":58,"props":1974,"children":1976},{"className":1975},[],[1977],{"type":32,"value":1978},"tsconfig.json",{"type":32,"value":1980}," 中开启 ",{"type":27,"tag":58,"props":1982,"children":1984},{"className":1983},[],[1985],{"type":32,"value":1986},"experimentalDecorators: true",{"type":32,"value":1988},"。5.0 之后开始支持 ECMAScript 标准的装饰器提案（Stage 3），默认不兼容。",{"type":27,"tag":28,"props":1990,"children":1991},{},[1992],{"type":32,"value":1993},"目前大部分遗留项目仍然在用传统装饰器，新项目可以逐步采用标准装饰器。本文以传统装饰器为例来说明模式——这些模式在新标准下同样适用，只是语法和参数有差异。",{"type":27,"tag":35,"props":1995,"children":1997},{"id":1996},"四种装饰器的签名",[1998],{"type":32,"value":1996},{"type":27,"tag":28,"props":2000,"children":2001},{},[2002],{"type":32,"value":2003},"传统装饰器有四类，每种接收的参数不同：",{"type":27,"tag":125,"props":2005,"children":2008},{"code":2006,"language":5,"meta":7,"className":2007},"// 类装饰器\ntype ClassDecorator = (target: Function) => void | Function\n\n// 方法装饰器\ntype MethodDecorator = \u003CT>(\n  target: T,\n  propertyKey: string | symbol,\n  descriptor: TypedPropertyDescriptor\u003Cany>\n) => void | TypedPropertyDescriptor\u003Cany>\n\n// 访问器装饰器（getter/setter）\ntype AccessorDecorator = \u003CT>(\n  target: T,\n  propertyKey: string | symbol,\n  descriptor: TypedPropertyDescriptor\u003Cany>\n) => void | TypedPropertyDescriptor\u003Cany>\n\n// 属性装饰器\ntype PropertyDecorator = (\n  target: Object,\n  propertyKey: string | symbol\n) => void\n",[219],[2009],{"type":27,"tag":58,"props":2010,"children":2011},{"__ignoreMap":7},[2012],{"type":32,"value":2006},{"type":27,"tag":28,"props":2014,"children":2015},{},[2016,2018,2024,2025,2031],{"type":32,"value":2017},"方法装饰器和属性装饰器虽然都接收 ",{"type":27,"tag":58,"props":2019,"children":2021},{"className":2020},[],[2022],{"type":32,"value":2023},"target",{"type":32,"value":351},{"type":27,"tag":58,"props":2026,"children":2028},{"className":2027},[],[2029],{"type":32,"value":2030},"propertyKey",{"type":32,"value":2032},"，但方法装饰器额外接收属性描述符（descriptor），所以可以修改方法的行为。属性装饰器没有 descriptor，只能做元数据附加。",{"type":27,"tag":35,"props":2034,"children":2036},{"id":2035},"模式-1自动日志方法装饰器",[2037],{"type":32,"value":2038},"模式 1：自动日志（方法装饰器）",{"type":27,"tag":28,"props":2040,"children":2041},{},[2042],{"type":32,"value":2043},"最常用的装饰器模式——给方法加日志，不侵入业务逻辑：",{"type":27,"tag":125,"props":2045,"children":2048},{"code":2046,"language":5,"meta":7,"className":2047},"function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value\n\n  descriptor.value = function (...args: any[]) {\n    console.log(`[${new Date().toISOString()}] ${propertyKey} called with:`, args)\n    const start = performance.now()\n    const result = originalMethod.apply(this, args)\n    const duration = performance.now() - start\n    console.log(`[${new Date().toISOString()}] ${propertyKey} completed in ${duration.toFixed(2)}ms`)\n    return result\n  }\n\n  return descriptor\n}\n\nclass UserService {\n  @log\n  async getUser(id: string) {\n    // 实际的业务逻辑\n    return db.users.find(id)\n  }\n\n  @log\n  async updateUser(id: string, data: Partial\u003CUser>) {\n    return db.users.update(id, data)\n  }\n}\n",[219],[2049],{"type":27,"tag":58,"props":2050,"children":2051},{"__ignoreMap":7},[2052],{"type":32,"value":2046},{"type":27,"tag":28,"props":2054,"children":2055},{},[2056],{"type":32,"value":2057},"输出：",{"type":27,"tag":125,"props":2059,"children":2061},{"code":2060},"[2026-06-04T10:00:00.000Z] getUser called with: [\"123\"]\n[2026-06-04T10:00:00.150Z] getUser completed in 150.23ms\n",[2062],{"type":27,"tag":58,"props":2063,"children":2064},{"__ignoreMap":7},[2065],{"type":32,"value":2060},{"type":27,"tag":28,"props":2067,"children":2068},{},[2069],{"type":32,"value":2070},"这种模式的优点是所有方法的日志行为一致，修改日志格式只需要改一个装饰器。而且每个装饰器是独立的——可以给重要方法加日志，给高频方法跳过。",{"type":27,"tag":35,"props":2072,"children":2074},{"id":2073},"模式-2输入校验方法装饰器-元数据",[2075],{"type":32,"value":2076},"模式 2：输入校验（方法装饰器 + 元数据）",{"type":27,"tag":28,"props":2078,"children":2079},{},[2080],{"type":32,"value":2081},"给方法参数加校验前置条件：",{"type":27,"tag":125,"props":2083,"children":2086},{"code":2084,"language":5,"meta":7,"className":2085},"type ValidationRule = (value: any) => string | null // null 表示通过\n\nfunction validate(rules: Record\u003Cstring, ValidationRule>) {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    const originalMethod = descriptor.value\n\n    descriptor.value = function (...args: any[]) {\n      // 通过参数名匹配规则（需要额外元数据来映射参数名）\n      const paramNames = getParamNames(originalMethod)\n      for (const [name, rule] of Object.entries(rules)) {\n        const index = paramNames.indexOf(name)\n        if (index === -1) continue\n        const error = rule(args[index])\n        if (error !== null) {\n          throw new ValidationError(`Validation failed for ${propertyKey}.${name}: ${error}`)\n        }\n      }\n\n      return originalMethod.apply(this, args)\n    }\n\n    return descriptor\n  }\n}\n\nclass UserController {\n  @validate({\n    email: (v: any) => typeof v === 'string' && v.includes('@') ? null : 'Invalid email',\n    age: (v: any) => typeof v === 'number' && v >= 0 && v \u003C= 150 ? null : 'Invalid age'\n  })\n  async createUser(email: string, age: number) {\n    // ...\n  }\n}\n",[219],[2087],{"type":27,"tag":58,"props":2088,"children":2089},{"__ignoreMap":7},[2090],{"type":32,"value":2084},{"type":27,"tag":28,"props":2092,"children":2093},{},[2094],{"type":32,"value":2095},"这里不需要在每个方法体里手写校验逻辑。如果将来校验规则变了，只需要改装饰器参数。",{"type":27,"tag":35,"props":2097,"children":2099},{"id":2098},"模式-3方法节流防抖方法装饰器",[2100],{"type":32,"value":2101},"模式 3：方法节流/防抖（方法装饰器）",{"type":27,"tag":28,"props":2103,"children":2104},{},[2105],{"type":32,"value":2106},"在 UI 事件或 API 调用场景下，节流和防抖是常见需求：",{"type":27,"tag":125,"props":2108,"children":2111},{"code":2109,"language":5,"meta":7,"className":2110},"function throttle(ms: number) {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    const originalMethod = descriptor.value\n    let lastCall = 0\n\n    descriptor.value = function (...args: any[]) {\n      const now = Date.now()\n      if (now - lastCall \u003C ms) return\n      lastCall = now\n      return originalMethod.apply(this, args)\n    }\n\n    return descriptor\n  }\n}\n\nclass AnalyticsTracker {\n  @throttle(1000) // 每秒最多上报一次\n  trackEvent(name: string, data: Record\u003Cstring, any>) {\n    // 发送分析事件\n    navigator.sendBeacon('/api/analytics', JSON.stringify({ name, data }))\n  }\n}\n",[219],[2112],{"type":27,"tag":58,"props":2113,"children":2114},{"__ignoreMap":7},[2115],{"type":32,"value":2109},{"type":27,"tag":35,"props":2117,"children":2119},{"id":2118},"模式-4依赖注入类装饰器-属性装饰器",[2120],{"type":32,"value":2121},"模式 4：依赖注入（类装饰器 + 属性装饰器）",{"type":27,"tag":28,"props":2123,"children":2124},{},[2125],{"type":32,"value":2126},"依赖注入是类级别装饰器的典型应用。先定义一个容器：",{"type":27,"tag":125,"props":2128,"children":2131},{"code":2129,"language":5,"meta":7,"className":2130},"const container = new Map\u003Cstring, any>()\n\nfunction Injectable() {\n  return function (target: any) {\n    // 标记这个类可以被注入\n    Reflect.defineMetadata('injectable', true, target)\n  }\n}\n\nfunction Inject(token: string) {\n  return function (target: any, propertyKey: string | symbol) {\n    // 在属性上存储依赖标记\n    Reflect.defineMetadata('inject', token, target, propertyKey)\n  }\n}\n\n// 自动装配\nfunction autoInject\u003CT extends { new (...args: any[]): any }>(target: T): T {\n  return class extends target {\n    constructor(...args: any[]) {\n      super(...args)\n      // 遍历原型链，查找需要注入的属性\n      const instance = this as any\n      const proto = Object.getPrototypeOf(instance)\n      for (const key of Object.getOwnPropertyNames(proto)) {\n        const token = Reflect.getMetadata('inject', proto, key)\n        if (token && container.has(token)) {\n          instance[key] = container.get(token)\n        }\n      }\n    }\n  }\n}\n\n@Injectable()\nclass Logger {\n  log(msg: string) { console.log(msg) }\n}\n\n@Injectable()\n@autoInject\nclass UserServiceDI {\n  @Inject('Logger')\n  private logger!: Logger\n\n  async getUser(id: string) {\n    this.logger.log(`Fetching user ${id}`)\n    // ...\n  }\n}\n",[219],[2132],{"type":27,"tag":58,"props":2133,"children":2134},{"__ignoreMap":7},[2135],{"type":32,"value":2129},{"type":27,"tag":28,"props":2137,"children":2138},{},[2139,2141,2147],{"type":32,"value":2140},"实际项目一般用 ",{"type":27,"tag":58,"props":2142,"children":2144},{"className":2143},[],[2145],{"type":32,"value":2146},"reflect-metadata",{"type":32,"value":2148}," 库来处理元数据操作。这里的关键是：属性装饰器只能标记\"需要注入什么\"，具体的注入逻辑在类装饰器（autoInject）中执行。",{"type":27,"tag":35,"props":2150,"children":2152},{"id":2151},"传统装饰器-vs-标准装饰器",[2153],{"type":32,"value":2154},"传统装饰器 vs 标准装饰器",{"type":27,"tag":28,"props":2156,"children":2157},{},[2158],{"type":32,"value":2159},"TypeScript 5.0 开始支持 ECMAScript 标准装饰器。主要区别：",{"type":27,"tag":549,"props":2161,"children":2162},{},[2163,2184],{"type":27,"tag":553,"props":2164,"children":2165},{},[2166],{"type":27,"tag":557,"props":2167,"children":2168},{},[2169,2174,2179],{"type":27,"tag":561,"props":2170,"children":2171},{},[2172],{"type":32,"value":2173},"方面",{"type":27,"tag":561,"props":2175,"children":2176},{},[2177],{"type":32,"value":2178},"传统装饰器（experimental）",{"type":27,"tag":561,"props":2180,"children":2181},{},[2182],{"type":32,"value":2183},"标准装饰器（Standard）",{"type":27,"tag":587,"props":2185,"children":2186},{},[2187,2210,2236,2254,2275],{"type":27,"tag":557,"props":2188,"children":2189},{},[2190,2195,2205],{"type":27,"tag":594,"props":2191,"children":2192},{},[2193],{"type":32,"value":2194},"tsconfig 配置",{"type":27,"tag":594,"props":2196,"children":2197},{},[2198,2200],{"type":32,"value":2199},"需要 ",{"type":27,"tag":58,"props":2201,"children":2203},{"className":2202},[],[2204],{"type":32,"value":1986},{"type":27,"tag":594,"props":2206,"children":2207},{},[2208],{"type":32,"value":2209},"不需要",{"type":27,"tag":557,"props":2211,"children":2212},{},[2213,2218,2227],{"type":27,"tag":594,"props":2214,"children":2215},{},[2216],{"type":32,"value":2217},"方法装饰器参数",{"type":27,"tag":594,"props":2219,"children":2220},{},[2221],{"type":27,"tag":58,"props":2222,"children":2224},{"className":2223},[],[2225],{"type":32,"value":2226},"(target, key, descriptor)",{"type":27,"tag":594,"props":2228,"children":2229},{},[2230],{"type":27,"tag":58,"props":2231,"children":2233},{"className":2232},[],[2234],{"type":32,"value":2235},"(target, context)",{"type":27,"tag":557,"props":2237,"children":2238},{},[2239,2244,2249],{"type":27,"tag":594,"props":2240,"children":2241},{},[2242],{"type":32,"value":2243},"返回值",{"type":27,"tag":594,"props":2245,"children":2246},{},[2247],{"type":32,"value":2248},"descriptor 或无",{"type":27,"tag":594,"props":2250,"children":2251},{},[2252],{"type":32,"value":2253},"新函数或 void",{"type":27,"tag":557,"props":2255,"children":2256},{},[2257,2262,2270],{"type":27,"tag":594,"props":2258,"children":2259},{},[2260],{"type":32,"value":2261},"元数据支持",{"type":27,"tag":594,"props":2263,"children":2264},{},[2265],{"type":27,"tag":58,"props":2266,"children":2268},{"className":2267},[],[2269],{"type":32,"value":2146},{"type":27,"tag":594,"props":2271,"children":2272},{},[2273],{"type":32,"value":2274},"需要装饰器自行管理",{"type":27,"tag":557,"props":2276,"children":2277},{},[2278,2283,2288],{"type":27,"tag":594,"props":2279,"children":2280},{},[2281],{"type":32,"value":2282},"与 Babel 兼容",{"type":27,"tag":594,"props":2284,"children":2285},{},[2286],{"type":32,"value":2287},"不一致",{"type":27,"tag":594,"props":2289,"children":2290},{},[2291],{"type":32,"value":2292},"一致",{"type":27,"tag":28,"props":2294,"children":2295},{},[2296],{"type":32,"value":2297},"标准装饰器的方法装饰器签名：",{"type":27,"tag":125,"props":2299,"children":2302},{"code":2300,"language":5,"meta":7,"className":2301},"function logged\u003CT extends (...args: any[]) => any>(\n  target: (this: void, ...args: Parameters\u003CT>) => ReturnType\u003CT>,\n  context: ClassMethodDecoratorContext\n) {\n  const methodName = String(context.name)\n\n  function replacementMethod(this: any, ...args: any[]) {\n    console.log(`LOG: Entering method '${methodName}'.`)\n    const result = target.call(this, ...args)\n    console.log(`LOG: Exiting method '${methodName}'.`)\n    return result\n  }\n\n  return replacementMethod\n}\n",[219],[2303],{"type":27,"tag":58,"props":2304,"children":2305},{"__ignoreMap":7},[2306],{"type":32,"value":2300},{"type":27,"tag":28,"props":2308,"children":2309},{},[2310,2312,2318,2320,2326,2327,2333,2334,2340,2341,2347],{"type":32,"value":2311},"context 对象提供了 ",{"type":27,"tag":58,"props":2313,"children":2315},{"className":2314},[],[2316],{"type":32,"value":2317},"name",{"type":32,"value":2319},"、",{"type":27,"tag":58,"props":2321,"children":2323},{"className":2322},[],[2324],{"type":32,"value":2325},"kind",{"type":32,"value":2319},{"type":27,"tag":58,"props":2328,"children":2330},{"className":2329},[],[2331],{"type":32,"value":2332},"static",{"type":32,"value":2319},{"type":27,"tag":58,"props":2335,"children":2337},{"className":2336},[],[2338],{"type":32,"value":2339},"private",{"type":32,"value":2319},{"type":27,"tag":58,"props":2342,"children":2344},{"className":2343},[],[2345],{"type":32,"value":2346},"addInitializer",{"type":32,"value":2348}," 等信息，比传统装饰器的签名更丰富。",{"type":27,"tag":35,"props":2350,"children":2351},{"id":726},[2352],{"type":32,"value":726},{"type":27,"tag":549,"props":2354,"children":2355},{},[2356,2377],{"type":27,"tag":553,"props":2357,"children":2358},{},[2359],{"type":27,"tag":557,"props":2360,"children":2361},{},[2362,2367,2372],{"type":27,"tag":561,"props":2363,"children":2364},{},[2365],{"type":32,"value":2366},"装饰器类型",{"type":27,"tag":561,"props":2368,"children":2369},{},[2370],{"type":32,"value":2371},"能做的事",{"type":27,"tag":561,"props":2373,"children":2374},{},[2375],{"type":32,"value":2376},"典型用途",{"type":27,"tag":587,"props":2378,"children":2379},{},[2380,2398,2416,2434],{"type":27,"tag":557,"props":2381,"children":2382},{},[2383,2388,2393],{"type":27,"tag":594,"props":2384,"children":2385},{},[2386],{"type":32,"value":2387},"类装饰器",{"type":27,"tag":594,"props":2389,"children":2390},{},[2391],{"type":32,"value":2392},"替换或包装构造函数",{"type":27,"tag":594,"props":2394,"children":2395},{},[2396],{"type":32,"value":2397},"依赖注入、注册到框架",{"type":27,"tag":557,"props":2399,"children":2400},{},[2401,2406,2411],{"type":27,"tag":594,"props":2402,"children":2403},{},[2404],{"type":32,"value":2405},"方法装饰器",{"type":27,"tag":594,"props":2407,"children":2408},{},[2409],{"type":32,"value":2410},"修改方法行为",{"type":27,"tag":594,"props":2412,"children":2413},{},[2414],{"type":32,"value":2415},"日志、校验、重试、缓存",{"type":27,"tag":557,"props":2417,"children":2418},{},[2419,2424,2429],{"type":27,"tag":594,"props":2420,"children":2421},{},[2422],{"type":32,"value":2423},"访问器装饰器",{"type":27,"tag":594,"props":2425,"children":2426},{},[2427],{"type":32,"value":2428},"修改 getter/setter",{"type":27,"tag":594,"props":2430,"children":2431},{},[2432],{"type":32,"value":2433},"响应式计算、延迟初始化",{"type":27,"tag":557,"props":2435,"children":2436},{},[2437,2442,2447],{"type":27,"tag":594,"props":2438,"children":2439},{},[2440],{"type":32,"value":2441},"属性装饰器",{"type":27,"tag":594,"props":2443,"children":2444},{},[2445],{"type":32,"value":2446},"附加元数据",{"type":27,"tag":594,"props":2448,"children":2449},{},[2450],{"type":32,"value":2451},"注入标记、序列化映射",{"type":27,"tag":28,"props":2453,"children":2454},{},[2455],{"type":32,"value":2456},"装饰器的核心价值在于横切关注点（cross-cutting concerns）的分离。日志、校验、权限检查这类逻辑不适合散落在每个方法体内——它们横跨多个方法，最好用一个装饰器统一管理。使用装饰器时要注意的一个原则是：它应该增强方法但不改变方法的语义，否则会引入难以调试的隐式行为。",{"title":7,"searchDepth":796,"depth":796,"links":2458},[2459,2460,2461,2462,2463,2464,2465],{"id":1996,"depth":799,"text":1996},{"id":2035,"depth":799,"text":2038},{"id":2073,"depth":799,"text":2076},{"id":2098,"depth":799,"text":2101},{"id":2118,"depth":799,"text":2121},{"id":2151,"depth":799,"text":2154},{"id":726,"depth":799,"text":726},"content:topics:typescript:typescript-decorators-real-world-validation-logging-di.md","topics/typescript/typescript-decorators-real-world-validation-logging-di.md","topics/typescript/typescript-decorators-real-world-validation-logging-di",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":2470,"image":18,"imageQuery":19,"pexelsPhotoId":20,"pexelsUrl":21,"featured":6,"readingTime":22,"body":2471,"_type":819,"_id":820,"_source":821,"_file":822,"_stem":823,"_extension":824},[13,14,15,16,17],{"type":24,"children":2472,"toc":3080},[2473,2477,2481,2485,2489,2517,2521,2525,2529,2533,2543,2551,2567,2571,2575,2583,2587,2594,2604,2608,2612,2620,2624,2628,2638,2645,2649,2653,2657,2665,2669,2673,2689,2697,2707,2711,2732,2740,2756,2760,2768,2772,2780,2790,2794,2798,2806,2822,2826,2834,2838,2842,2846,2854,2870,2874,2878,3025,3029,3033,3076],{"type":27,"tag":28,"props":2474,"children":2475},{},[2476],{"type":32,"value":33},{"type":27,"tag":35,"props":2478,"children":2479},{"id":37},[2480],{"type":32,"value":37},{"type":27,"tag":28,"props":2482,"children":2483},{},[2484],{"type":32,"value":44},{"type":27,"tag":46,"props":2486,"children":2487},{"id":48},[2488],{"type":32,"value":51},{"type":27,"tag":28,"props":2490,"children":2491},{},[2492,2493,2498,2499,2504,2505,2510,2511,2516],{"type":32,"value":56},{"type":27,"tag":58,"props":2494,"children":2496},{"className":2495},[],[2497],{"type":32,"value":63},{"type":32,"value":65},{"type":27,"tag":58,"props":2500,"children":2502},{"className":2501},[],[2503],{"type":32,"value":71},{"type":32,"value":73},{"type":27,"tag":58,"props":2506,"children":2508},{"className":2507},[],[2509],{"type":32,"value":63},{"type":32,"value":80},{"type":27,"tag":58,"props":2512,"children":2514},{"className":2513},[],[2515],{"type":32,"value":86},{"type":32,"value":88},{"type":27,"tag":46,"props":2518,"children":2519},{"id":91},[2520],{"type":32,"value":94},{"type":27,"tag":28,"props":2522,"children":2523},{},[2524],{"type":32,"value":99},{"type":27,"tag":28,"props":2526,"children":2527},{},[2528],{"type":32,"value":104},{"type":27,"tag":35,"props":2530,"children":2531},{"id":107},[2532],{"type":32,"value":110},{"type":27,"tag":28,"props":2534,"children":2535},{},[2536,2537,2542],{"type":32,"value":115},{"type":27,"tag":58,"props":2538,"children":2540},{"className":2539},[],[2541],{"type":32,"value":121},{"type":32,"value":123},{"type":27,"tag":125,"props":2544,"children":2546},{"code":127,"language":128,"meta":7,"className":2545},[130],[2547],{"type":27,"tag":58,"props":2548,"children":2549},{"__ignoreMap":7},[2550],{"type":32,"value":127},{"type":27,"tag":28,"props":2552,"children":2553},{},[2554,2555,2560,2561,2566],{"type":32,"value":140},{"type":27,"tag":58,"props":2556,"children":2558},{"className":2557},[],[2559],{"type":32,"value":146},{"type":32,"value":148},{"type":27,"tag":58,"props":2562,"children":2564},{"className":2563},[],[2565],{"type":32,"value":154},{"type":32,"value":156},{"type":27,"tag":35,"props":2568,"children":2569},{"id":159},[2570],{"type":32,"value":162},{"type":27,"tag":28,"props":2572,"children":2573},{},[2574],{"type":32,"value":167},{"type":27,"tag":125,"props":2576,"children":2578},{"code":170,"language":171,"meta":7,"className":2577},[173],[2579],{"type":27,"tag":58,"props":2580,"children":2581},{"__ignoreMap":7},[2582],{"type":32,"value":170},{"type":27,"tag":28,"props":2584,"children":2585},{},[2586],{"type":32,"value":183},{"type":27,"tag":125,"props":2588,"children":2589},{"code":186},[2590],{"type":27,"tag":58,"props":2591,"children":2592},{"__ignoreMap":7},[2593],{"type":32,"value":186},{"type":27,"tag":28,"props":2595,"children":2596},{},[2597,2598,2603],{"type":32,"value":196},{"type":27,"tag":58,"props":2599,"children":2601},{"className":2600},[],[2602],{"type":32,"value":71},{"type":32,"value":203},{"type":27,"tag":35,"props":2605,"children":2606},{"id":206},[2607],{"type":32,"value":209},{"type":27,"tag":28,"props":2609,"children":2610},{},[2611],{"type":32,"value":214},{"type":27,"tag":125,"props":2613,"children":2615},{"code":217,"language":5,"meta":7,"className":2614},[219],[2616],{"type":27,"tag":58,"props":2617,"children":2618},{"__ignoreMap":7},[2619],{"type":32,"value":217},{"type":27,"tag":28,"props":2621,"children":2622},{},[2623],{"type":32,"value":229},{"type":27,"tag":35,"props":2625,"children":2626},{"id":232},[2627],{"type":32,"value":235},{"type":27,"tag":28,"props":2629,"children":2630},{},[2631,2632,2637],{"type":32,"value":240},{"type":27,"tag":58,"props":2633,"children":2635},{"className":2634},[],[2636],{"type":32,"value":71},{"type":32,"value":247},{"type":27,"tag":125,"props":2639,"children":2640},{"code":250},[2641],{"type":27,"tag":58,"props":2642,"children":2643},{"__ignoreMap":7},[2644],{"type":32,"value":250},{"type":27,"tag":28,"props":2646,"children":2647},{},[2648],{"type":32,"value":260},{"type":27,"tag":46,"props":2650,"children":2651},{"id":263},[2652],{"type":32,"value":266},{"type":27,"tag":28,"props":2654,"children":2655},{},[2656],{"type":32,"value":271},{"type":27,"tag":125,"props":2658,"children":2660},{"code":274,"language":5,"meta":7,"className":2659},[219],[2661],{"type":27,"tag":58,"props":2662,"children":2663},{"__ignoreMap":7},[2664],{"type":32,"value":274},{"type":27,"tag":28,"props":2666,"children":2667},{},[2668],{"type":32,"value":285},{"type":27,"tag":46,"props":2670,"children":2671},{"id":288},[2672],{"type":32,"value":291},{"type":27,"tag":28,"props":2674,"children":2675},{},[2676,2677,2682,2683,2688],{"type":32,"value":296},{"type":27,"tag":58,"props":2678,"children":2680},{"className":2679},[],[2681],{"type":32,"value":302},{"type":32,"value":304},{"type":27,"tag":58,"props":2684,"children":2686},{"className":2685},[],[2687],{"type":32,"value":310},{"type":32,"value":312},{"type":27,"tag":125,"props":2690,"children":2692},{"code":315,"language":5,"meta":7,"className":2691},[219],[2693],{"type":27,"tag":58,"props":2694,"children":2695},{"__ignoreMap":7},[2696],{"type":32,"value":315},{"type":27,"tag":28,"props":2698,"children":2699},{},[2700,2701,2706],{"type":32,"value":326},{"type":27,"tag":58,"props":2702,"children":2704},{"className":2703},[],[2705],{"type":32,"value":332},{"type":32,"value":334},{"type":27,"tag":35,"props":2708,"children":2709},{"id":337},[2710],{"type":32,"value":340},{"type":27,"tag":28,"props":2712,"children":2713},{},[2714,2719,2720,2725,2726,2731],{"type":27,"tag":58,"props":2715,"children":2717},{"className":2716},[],[2718],{"type":32,"value":349},{"type":32,"value":351},{"type":27,"tag":58,"props":2721,"children":2723},{"className":2722},[],[2724],{"type":32,"value":357},{"type":32,"value":359},{"type":27,"tag":58,"props":2727,"children":2729},{"className":2728},[],[2730],{"type":32,"value":365},{"type":32,"value":367},{"type":27,"tag":125,"props":2733,"children":2735},{"code":370,"language":5,"meta":7,"className":2734},[219],[2736],{"type":27,"tag":58,"props":2737,"children":2738},{"__ignoreMap":7},[2739],{"type":32,"value":370},{"type":27,"tag":28,"props":2741,"children":2742},{},[2743,2744,2749,2750,2755],{"type":32,"value":381},{"type":27,"tag":58,"props":2745,"children":2747},{"className":2746},[],[2748],{"type":32,"value":365},{"type":32,"value":388},{"type":27,"tag":58,"props":2751,"children":2753},{"className":2752},[],[2754],{"type":32,"value":394},{"type":32,"value":396},{"type":27,"tag":35,"props":2757,"children":2758},{"id":399},[2759],{"type":32,"value":399},{"type":27,"tag":125,"props":2761,"children":2763},{"code":404,"language":128,"meta":7,"className":2762},[130],[2764],{"type":27,"tag":58,"props":2765,"children":2766},{"__ignoreMap":7},[2767],{"type":32,"value":404},{"type":27,"tag":28,"props":2769,"children":2770},{},[2771],{"type":32,"value":415},{"type":27,"tag":125,"props":2773,"children":2775},{"code":418,"language":5,"meta":7,"className":2774},[219],[2776],{"type":27,"tag":58,"props":2777,"children":2778},{"__ignoreMap":7},[2779],{"type":32,"value":418},{"type":27,"tag":28,"props":2781,"children":2782},{},[2783,2784,2789],{"type":32,"value":429},{"type":27,"tag":58,"props":2785,"children":2787},{"className":2786},[],[2788],{"type":32,"value":435},{"type":32,"value":437},{"type":27,"tag":35,"props":2791,"children":2792},{"id":440},[2793],{"type":32,"value":440},{"type":27,"tag":46,"props":2795,"children":2796},{"id":445},[2797],{"type":32,"value":448},{"type":27,"tag":125,"props":2799,"children":2801},{"code":451,"language":5,"meta":7,"className":2800},[219],[2802],{"type":27,"tag":58,"props":2803,"children":2804},{"__ignoreMap":7},[2805],{"type":32,"value":451},{"type":27,"tag":28,"props":2807,"children":2808},{},[2809,2810,2815,2816,2821],{"type":32,"value":462},{"type":27,"tag":58,"props":2811,"children":2813},{"className":2812},[],[2814],{"type":32,"value":468},{"type":32,"value":470},{"type":27,"tag":58,"props":2817,"children":2819},{"className":2818},[],[2820],{"type":32,"value":468},{"type":32,"value":477},{"type":27,"tag":46,"props":2823,"children":2824},{"id":480},[2825],{"type":32,"value":483},{"type":27,"tag":125,"props":2827,"children":2829},{"code":486,"language":5,"meta":7,"className":2828},[219],[2830],{"type":27,"tag":58,"props":2831,"children":2832},{"__ignoreMap":7},[2833],{"type":32,"value":486},{"type":27,"tag":28,"props":2835,"children":2836},{},[2837],{"type":32,"value":497},{"type":27,"tag":46,"props":2839,"children":2840},{"id":500},[2841],{"type":32,"value":503},{"type":27,"tag":28,"props":2843,"children":2844},{},[2845],{"type":32,"value":508},{"type":27,"tag":125,"props":2847,"children":2849},{"code":511,"language":5,"meta":7,"className":2848},[219],[2850],{"type":27,"tag":58,"props":2851,"children":2852},{"__ignoreMap":7},[2853],{"type":32,"value":511},{"type":27,"tag":28,"props":2855,"children":2856},{},[2857,2858,2863,2864,2869],{"type":32,"value":522},{"type":27,"tag":58,"props":2859,"children":2861},{"className":2860},[],[2862],{"type":32,"value":146},{"type":32,"value":529},{"type":27,"tag":58,"props":2865,"children":2867},{"className":2866},[],[2868],{"type":32,"value":535},{"type":32,"value":537},{"type":27,"tag":35,"props":2871,"children":2872},{"id":540},[2873],{"type":32,"value":540},{"type":27,"tag":28,"props":2875,"children":2876},{},[2877],{"type":32,"value":547},{"type":27,"tag":549,"props":2879,"children":2880},{},[2881,2907],{"type":27,"tag":553,"props":2882,"children":2883},{},[2884],{"type":27,"tag":557,"props":2885,"children":2886},{},[2887,2891,2895,2899,2903],{"type":27,"tag":561,"props":2888,"children":2889},{},[2890],{"type":32,"value":565},{"type":27,"tag":561,"props":2892,"children":2893},{},[2894],{"type":32,"value":570},{"type":27,"tag":561,"props":2896,"children":2897},{},[2898],{"type":32,"value":575},{"type":27,"tag":561,"props":2900,"children":2901},{},[2902],{"type":32,"value":580},{"type":27,"tag":561,"props":2904,"children":2905},{},[2906],{"type":32,"value":585},{"type":27,"tag":587,"props":2908,"children":2909},{},[2910,2933,2956,2979,3002],{"type":27,"tag":557,"props":2911,"children":2912},{},[2913,2917,2921,2925,2929],{"type":27,"tag":594,"props":2914,"children":2915},{},[2916],{"type":32,"value":598},{"type":27,"tag":594,"props":2918,"children":2919},{},[2920],{"type":32,"value":603},{"type":27,"tag":594,"props":2922,"children":2923},{},[2924],{"type":32,"value":608},{"type":27,"tag":594,"props":2926,"children":2927},{},[2928],{"type":32,"value":608},{"type":27,"tag":594,"props":2930,"children":2931},{},[2932],{"type":32,"value":608},{"type":27,"tag":557,"props":2934,"children":2935},{},[2936,2940,2944,2948,2952],{"type":27,"tag":594,"props":2937,"children":2938},{},[2939],{"type":32,"value":624},{"type":27,"tag":594,"props":2941,"children":2942},{},[2943],{"type":32,"value":629},{"type":27,"tag":594,"props":2945,"children":2946},{},[2947],{"type":32,"value":634},{"type":27,"tag":594,"props":2949,"children":2950},{},[2951],{"type":32,"value":639},{"type":27,"tag":594,"props":2953,"children":2954},{},[2955],{"type":32,"value":644},{"type":27,"tag":557,"props":2957,"children":2958},{},[2959,2963,2967,2971,2975],{"type":27,"tag":594,"props":2960,"children":2961},{},[2962],{"type":32,"value":15},{"type":27,"tag":594,"props":2964,"children":2965},{},[2966],{"type":32,"value":656},{"type":27,"tag":594,"props":2968,"children":2969},{},[2970],{"type":32,"value":656},{"type":27,"tag":594,"props":2972,"children":2973},{},[2974],{"type":32,"value":665},{"type":27,"tag":594,"props":2976,"children":2977},{},[2978],{"type":32,"value":670},{"type":27,"tag":557,"props":2980,"children":2981},{},[2982,2986,2990,2994,2998],{"type":27,"tag":594,"props":2983,"children":2984},{},[2985],{"type":32,"value":678},{"type":27,"tag":594,"props":2987,"children":2988},{},[2989],{"type":32,"value":683},{"type":27,"tag":594,"props":2991,"children":2992},{},[2993],{"type":32,"value":688},{"type":27,"tag":594,"props":2995,"children":2996},{},[2997],{"type":32,"value":693},{"type":27,"tag":594,"props":2999,"children":3000},{},[3001],{"type":32,"value":693},{"type":27,"tag":557,"props":3003,"children":3004},{},[3005,3009,3013,3017,3021],{"type":27,"tag":594,"props":3006,"children":3007},{},[3008],{"type":32,"value":705},{"type":27,"tag":594,"props":3010,"children":3011},{},[3012],{"type":32,"value":710},{"type":27,"tag":594,"props":3014,"children":3015},{},[3016],{"type":32,"value":639},{"type":27,"tag":594,"props":3018,"children":3019},{},[3020],{"type":32,"value":719},{"type":27,"tag":594,"props":3022,"children":3023},{},[3024],{"type":32,"value":644},{"type":27,"tag":35,"props":3026,"children":3027},{"id":726},[3028],{"type":32,"value":726},{"type":27,"tag":28,"props":3030,"children":3031},{},[3032],{"type":32,"value":733},{"type":27,"tag":735,"props":3034,"children":3035},{},[3036,3044,3052,3060,3068],{"type":27,"tag":739,"props":3037,"children":3038},{},[3039,3043],{"type":27,"tag":743,"props":3040,"children":3041},{},[3042],{"type":32,"value":747},{"type":32,"value":749},{"type":27,"tag":739,"props":3045,"children":3046},{},[3047,3051],{"type":27,"tag":743,"props":3048,"children":3049},{},[3050],{"type":32,"value":757},{"type":32,"value":759},{"type":27,"tag":739,"props":3053,"children":3054},{},[3055,3059],{"type":27,"tag":743,"props":3056,"children":3057},{},[3058],{"type":32,"value":767},{"type":32,"value":769},{"type":27,"tag":739,"props":3061,"children":3062},{},[3063,3067],{"type":27,"tag":743,"props":3064,"children":3065},{},[3066],{"type":32,"value":777},{"type":32,"value":779},{"type":27,"tag":739,"props":3069,"children":3070},{},[3071,3075],{"type":27,"tag":743,"props":3072,"children":3073},{},[3074],{"type":32,"value":787},{"type":32,"value":789},{"type":27,"tag":28,"props":3077,"children":3078},{},[3079],{"type":32,"value":794},{"title":7,"searchDepth":796,"depth":796,"links":3081},[3082,3086,3087,3088,3089,3093,3094,3095,3100,3101],{"id":37,"depth":799,"text":37,"children":3083},[3084,3085],{"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":3090},[3091,3092],{"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":3096},[3097,3098,3099],{"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},1780533153712]