前几天接了个老项目的维护任务顺手把 TypeScript 从 4.9 往上升了一档。编译窗口里突然冒出两行以前从没见过的英文警告一条是关于baseUrl弃用的另一条是关于moduleResolutionnode10弃用的结尾都缀着一句“并将停止在 TypeScript 7.0 中运行”。说实话大部分人对这种“未来威胁”都有点免疫力第一反应多半是先放着反正还能跑。但我盯着这两行字看了一会儿后背有点发凉——老项目里baseUrl和 node10 这套解析逻辑早就是默认配置的一部分整个项目几十个文件的相对路径、别名引用全挂在上面真等到 TS 7.0 那天这些配置会被一次性摘掉项目能不能编译都是个问题。这篇文章就把这次升级和迁移的完整过程记录下来也算是我准备写的 TypeScript 学习系列的第一篇。内容不追求讲完整个语言而是从工程化配置切入重点拆解tsconfig.json里那些“平时不觉得重要、出问题时让你抓狂”的选项baseUrl、moduleResolution、paths以及它们和 Vite、vue-tsc、Electron 打包之间的联动关系。适合正在用 TypeScript 5.x、项目里还有老配置、或者刚接触 TS 工程配置的读者。1. 版本升级引发的“弃用警告”到底是什么这两条警告不是 bug也不是误报。它是 TypeScript 官方在 5.0 之后陆续加进来的“提前通知”意思是你正在用的某项老配置已经进了淘汰名单短时间还能继续工作但未来大版本会直接删掉。被点名的就是baseUrl和moduleResolutionnode10。要搞清楚为什么会被弃用得先知道这两个东西当年是怎么来的又为什么会变成历史包袱。1.1 baseUrl从“便利贴”到“历史包袱”baseUrl是 TypeScript 早年提供的一个路径解析参数作用非常朴素指定一个根目录让项目里所有非相对路径的模块引用都从这个根目录开始找。比如你在tsconfig.json里写了baseUrl: ./src代码里就能直接这样写import { formatDate } from utils/date编译器会自动拼成项目根目录下的src/utils/date。早年间没有paths的时候baseUrl几乎是唯一的“路径别名”方案配合绝对路径风格确实让一堆../../../../utils的写法清爽了不少。这也是它能流行起来的最主要原因——它解决了一个真实痛点。但随着 TypeScript 版本迭代baseUrl的问题越来越明显。首先是它和现代模块解析方式产生冲突。现在主流的 Node.js 生态和打包工具都开始遵循package.json里的exports字段来约束模块入口而baseUrl的解析规则是“从根目录盲找”完全无视exports。这会导致同一个引用在编辑器里能跳转、在 tsc 检查下能通过但到了真正的运行环境和打包环节却找不到模块两头各说各话。其次TS 4.1 之后paths得到了增强它已经可以脱离baseUrl独立工作直接基于tsconfig.json所在目录解析。也就是说别名这个原本需要baseUrl支持的功能现在有了更干净的替代方案。继续保留baseUrl不但没有增量收益反而成为一坨干扰项还会影响 TS 7.0 的模块解析重构所以官方决定移除。这里特别提醒一句如果你在项目里用的是baseUrlpaths组合那么升级到 TS 5.x 后完全可以先删掉baseUrl保留paths并改成相对路径写法让它基于tsconfig.json的目录解析。这样行为基本不变但配置已经提前进入新体系。1.2 moduleResolutionnode10 的来龙去脉node10这个名词现在看着陌生其实它就是老版本 TypeScript 里的moduleResolution: node。因为原来的名字太“占坑”官方在 5.0 引入了更精确的命名规则把老的node模式改名成node10同时加入了node16、nodenext、bundler这些新模式。这个改动本身没问题问题在于大量项目一直沿用旧配置没动过。node10 模式是一套为 CommonJS 时代设计的模块解析算法它模仿了早期 Node.js 的查找逻辑从当前文件所在目录开始先尝试补全扩展名.ts、.tsx、.d.ts再尝试找index文件找不到就往上一级目录的node_modules里继续搜。这套机制在 npm 包普遍没有exports字段的时期非常实用放今天就很尴尬了。现在一个规范的 npm 包基本都有exports字段用来精确声明哪些入口可以对外、哪些内部文件不能直接引用而 node10 模式完全不理解exports它会绕过这些约束直接按文件名去找结果就是模块解析结果和真实运行结果经常对不上。最常见的情况是类型检查通过了代码跑起来却报模块不存在原因就是 tsc 找了错的入口。我在这次的升级中把项目从 node10 切到了bundler模式问题基本清零。bundler模式正是为 Vite、Webpack、Rollup 这类打包器场景设计的它会遵循exports字段同时保留无扩展名解析等打包器特有的行为和前端项目的实际解析逻辑保持一致。换成这个模式后编辑器提示、tsc 检查、构建结果三者的口径终于统一了。解析模式何时使用优点代价node10老项目、CommonJS、无 exports 的旧 npm 包兼容性好什么都能找到无视 exports结果不可靠已弃用node16 / nodenextNode.js 原生项目要求严格遵循 Node 行为完全对齐 Node 的解析规则支持 ESM/CJS 双模型前端项目用起来略繁琐很多写法要调整bundlerVite、Webpack、Rollup 等打包器项目尊重 exports也能兼容无扩展名、目录引用等打包器习惯不能直接用于纯 Node 运行场景2. 从 5.x 到 7.0 的迁移实操知道了背景接下来就是动手改。我建议的迁移顺序不是“看到警告就删配置”而是“先摸清家底再分步调整最后用构建命令验证”。这套思路听起来保守但实际过程中能帮你少踩很多坑因为tsconfig.json里的选项往往是联动的牵一发而动全身。2.1 迁移前先摸清家底动手之前先把项目的依赖和配置梳理一遍。我自己习惯先跑几条命令npm ls typescript npx tsc --version npx tsc --showConfignpm ls typescript能看出项目安装的 TS 版本以及是否存在版本冲突tsc --showConfig会输出最终生效的完整配置适合确认没有其他配置文件覆盖你的设置npx tsc --noEmit则用来记录当前项目还有多少个类型错误作为迁移前的基线参考。重点检查这几项内容tsconfig.json里的module、moduleResolution、baseUrl、paths、type字段以及构建工具是 Vite、Webpack 还是纯 Node 环境。如果项目同时存在tsconfig.json、tsconfig.node.json、tsconfig.build.json这种多配置文件结构每个文件都要看一遍因为警告可能只出现在某一个配置片段里而它影响的却是整个构建流程。我在这次维护中就遇到一个情况主配置文件已经改得很干净了但配套的tsconfig.node.json里还有个残留的baseUrl单独看无伤大雅合在一起就一直在报警告。还要注意一点本地开发时编辑器提示版本和命令行版本可能不一致。比如 VSCode 用的是内置 TS 版本而你项目里装的是另一个版本这种情况下tsc --noEmit是准的编辑器提示则可能不准。建议在项目根目录装一个 TypeScript并让编辑器使用 workspace 版本避免两边结论不一致。2.2 tsconfig.json 逐步改造摸清家底后就可以动手改了。下面这份清单对应的是前端打包器项目Vite 或 Webpack用的是这套最常见的组合改造前{ compilerOptions: { target: ES2020, module: esnext, moduleResolution: node, baseUrl: ./, paths: { /*: [src/*] } } }改造后{ compilerOptions: { target: ES2020, module: esnext, moduleResolution: bundler, paths: { /*: [./src/*] } } }改动的地方有三处。第一moduleResolution从nodenode10改成bundler。这是让模块解析规则对齐打包器行为的关键一步改完以后exports字段生效依赖入口更准确。第二删掉baseUrl。第三调整paths因为删掉baseUrl后paths的基准目录变成了tsconfig.json所在目录原来的src/*要显式改成./src/*。改完之后马上跑npx tsc --noEmit看有没有路径解析相关的新报错。正常情况下改动前后的类型检查结果应该完全一致如果突然出现一堆Cannot find module那多半是paths的基准目录理解错了或者有某些相对路径依赖了原来的baseUrl行为。再补一条对照项如果项目用了 ESLint检查配置里的import/resolver或tsconfig-paths相关插件它们的解析规则也要跟着同步。2.3 别名交给谁管打包器的分工有一类问题是迁移时最容易忽略的TypeScript 检查通过了但启动项目后运行时路径找不到。原因很简单TS 和打包器各有各的解析规则。tsconfig.json里的paths只对 tsc 和编辑器生效它不会去改 Vite 或 Webpack 的模块解析。也就是说如果你只改了tsconfig.json而打包器配置里没有同步配置别名那么项目运行时依然会按实际路径去node_modules里找/xxx结果自然是一堆错误。Vite 项目里别名通常在vite.config.ts中配置import { defineConfig } from vite import { fileURLToPath, URL } from node:url export default defineConfig({ resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } } })Webpack 项目则在resolve.alias中配置const path require(path) module.exports { resolve: { alias: { : path.resolve(__dirname, src) } } }职责边界理清以后迁移思路就简单了TS 侧负责类型检查和编辑器提示打包器侧负责运行时解析两边配置保持一致的路径规则就行。这个原则在改造前就应该刻在脑子里很多人被这个问题卡了半天本质就是因为把两套解析体系混为一谈了。3. 生态适配vue-tsc、electron 打包与 react 场景光改tsconfig.json一套还不够现实里的项目大多绑定了框架和工具链。这次踩坑的过程中vue-tsc 和 Electron 打包这两个场景给我留下的印象最深React 项目倒是相对平静。下面逐个说。3.1 vue-tsc 与 TypeScript 的版本对应vue-tsc 是 Vue 项目里做单文件组件类型检查的专用工具。它的版本和 TypeScript 版本之间有很强的依赖关系不是随便配就能跑起来的。这次维护的项目里锁定的组合是vue-tsc: ^1.8.27、typescript: ^5.3.3这个搭配在 5.3 时代非常典型因为 vue-tsc 1.8.x 就是为了适配 TS 5.x 而发布的包含了对 Vue 3.3 宏定义和泛型组件的完整支持。但如果你把 TypeScript 一步拉到 5.5 或更高再回头看旧版 vue-tsc可能就会遇到类型定义查找失败、SFC 里自定义组件属性全部报错这类问题。其实网络热词里提到的“vue 类型工具与现有 typescript 7 不兼容”也是同一类问题的延伸新版 TS 在类型推断细节上一直在变化而 Vue 的类型工具要逐个适配。就我个人的实操经验在 Vue 项目里升级 TS 时最好是“TS 和 vue-tsc 一起升”不要单独动一个。升级前先用npm ls vue-tsc看当前锁定版本再去 vue-tsc 的发布记录里确认它对当前 TS 版本的兼容范围这样才能避免版本错配。Electron 打包场景里vue-tsc 通常被放在build脚本里用来在打包前做一次完整的类型检查。很多项目的package.json里是这样写的{ scripts: { build: vue-tsc --noEmit vite build } }这种写法没毛病但会给迁移增加一个隐藏约束你升级 TS 后vue-tsc必须先过了自己那一关才有机会跑到vite build。所以我在这次改动中特意把vue-tsc --noEmit单独拆出来跑了一遍发现它就是整个链条里最容易出问题的点。3.2 electron 项目的实际配置Electron 项目相比纯前端项目更复杂一些因为它同时包含主进程、预加载脚本和渲染进程不同环境的模块解析目标还不一样。如果项目主进程用的是 CommonJS 风格渲染进程用的是 Vite 打包那么一份tsconfig.json往往不够用。合理做法是拆多个配置文件主进程一份、渲染进程一份、公共共享配置一份。我给你们一个可以直接参考的分层结构{ files: [], references: [ { path: ./tsconfig.node.json }, { path: ./tsconfig.web.json } ] }tsconfig.node.json负责主进程和 preload 脚本使用module: commonjs或nodenexttsconfig.web.json负责渲染进程使用module: esnext、moduleResolution: bundler。两个子配置分别维护各自的paths互不干扰。这种 Project References 的方式是 Electron Vite 项目里比较标准的结构。Electron 打包时还有个比较隐晦的坑如果你的主进程代码里引用了electron模块但tsconfig.node.json里没有加types: [node]那么process、__dirname这些 Node 全局变量会被识别成错误。这个不是这次迁移引入的问题但我在排查糯米团一样的错误列表时确实被这种与版本升级无关的“存量债”分散了注意力。建议迁移前先把这类错误清零再处理弃用警告否则新旧问题混在一起排查思路会很乱。3.3 react typescript 场景React 项目相对 Vue 来说要省心一些因为 React 类型系统和 TS 的配合一直都比较直接而react-scripts、Vite 这些工具链也长期把 TS 配置当成一等公民。React 项目升级 TS 后最常见的两个问题是 JSX 配置和“重复类型定义”。JSX 方面只要 TS 5.x 下jsx设置为react-jsx构建工具就不会要求手动import React from react新项目基本都是这个配置。老项目如果是jsx: react建议顺便把它一起改掉省得每次在文件顶部写无用的 import。重复类型定义的问题更隐蔽通常出现在安装了多个版本的types/react或types/react-dom时两个版本交错会让组件 props 报出莫名其妙的类型冲突。排查方法也很简单npm ls types/react看有没有重复有就去掉多余版本只保留一个。整体上 React 项目的 TS 迁移改完moduleResolution和删掉baseUrl之后基本就稳了很少有 vue-tsc 那种连锁反应。4. 常见问题速查与避坑记录这次迁移过程中我把自己踩过和看别人踩过的坑整理成了一张速查表。如果你也在做 TypeScript 5.x 到 7.0 的预备迁移可以先对照着排查一遍。警告/报错出现原因处理方式选项“baseurl”已弃用tsconfig.json中配置了baseUrl删除baseUrl将paths改为相对tsconfig.json目录的写法选项“moduleresolutionnode10”已弃用moduleResolution仍为旧版node修改为bundler或nodenext视项目类型而定Cannot find module /xxx/yyyTSpaths与打包器 alias 不一致同时检查并同步tsconfig.json与 Vite/Webpack 的别名配置vue-tsc 报类型定义文件缺失vue-tsc 与 TS 版本不匹配升级 vue-tsc 到支持当前 TS 的版本建议按官方发布说明锁定组合.vue 文件无法识别缺少vue模块的 shim 声明在env.d.ts中加入declare module *.vue并确认vue-tsc版本小于 TS 主版本即可除了表格里的内容几个有价值的个人经验也分享一下。第一升级 TypeScript 这种基础依赖一定要预留出充足时间专门处理警告。不要“顺手升级完就提交”因为弃用警告虽然不会立刻让项目崩溃但它们通常会沉积到下一个核心版本才爆发到那时再改风险面就大了很多。我自己这次就是把所有警告当作 error 来处理一条一条清掉整个过程花了一个下午但换来了后续几个版本的安心。第二配置项要理解之后再动手。很多人一看到弃用警告就直接到网上复制最新的tsconfig.json然后发现构建报错更多。问题不在于新版配置不好而在于你没有理解项目为什么需要那些旧配置。稳妥做法是逐项修改、逐项验证改完一处跑一次tsc --noEmit确认没有新问题再改下一处。这种“小步快跑”的方式虽然看起来慢实际是最快的。第三paths里的路径基准是tsconfig.json所在目录不是项目根目录。删除baseUrl后原本/*: [src/*]这种写法如果没有加./前缀就可能会解析失败。新版规范建议统一写成/*: [./src/*]这个细节虽然小但很容易被忽略。另外有一些项目在paths里会写扩展名比如[src/*.ts]这种做法在老的解析模式下有时能糊弄过去但切到bundler模式后会变得更加不可靠。建议paths只写可解析的目录模式让解析器自动补全扩展名不要人为锁死。关于 vue 类型工具和 TS 7 的兼容性问题微信里不少人也在讨论。我的看法是等到 TS 7 正式发布的时候Vue 官方生态大概率会同步跟进适配。现在要做的不是焦虑而是提前把配置层面该清理的老债还掉等新版本出来就能直接从上一代配置无缝切到下一代配置中间不做任何额外处理。这次动手清理老配置最大的收获倒不是项目本身跑得更稳了而是对 TypeScript 的模块解析体系有了完整认识。以前只知道配置能跑不明白背后为什么这么设计现在反过来看像baseUrl这种从“救火队员”变成“历史包袱”的过程其实是很多编程工具演进的缩影。以后写 TypeScript 学习系列我也会继续沿着这种“搞懂原理再动手”的思路往下写比如类型体操的底层逻辑、泛型在设计真实组件时的应用、以及构建工具如何与类型系统协同等等都会慢慢展开。
