内容提要
TypeScript 5.0 新增 bundler 模块解析策略,解决 Node 不支持 exports、node16/nodenext 强制写扩展名的问题。文章梳理模块解析策略演进(classic、node、node16、nodenext、bundler),重点讲解 package.json 的 main、module、typesVersions 与 exports 字段,包括子路径、通配符、条件导出、优先级及 types 条件。指出 exports 是模块解析最终标准,但社区实现常有优先级 bug,推荐新库使用 bundler 加 exports。
延伸解读
为什么需要 bundler 解析策略
TypeScript 5.0 新增的 bundler 策略,是为了解决现有策略在 vite 等工具下的痛点。node 策略不支持 exports,无法利用现代包导出规则;node16/nodenext 虽支持 exports,却强制相对导入写扩展名,与 vite 等打包工具的实际用法冲突。bundler 允许使用 exports 管理类型,同时相对导入可不写扩展名,更贴合前端开发习惯。
exports 优先级:社区实现常有 bug
exports 的匹配规则复杂,尤其是多个 pattern 都能匹配时,需要选择最具体的那个。文章指出,目前主流解析库如 resolve.exports、@rollup/plugin-node-resolve、nodejs-resolver 在处理优先级时都有 bug,只有 webpack 的 enhanced-resolve 能正确解析部分用例,但它也有自己的局限。因此,依赖 exports 的复杂配置时需谨慎,并借助 publint 等工具检测。
typesVersions 与 exports 的取舍
typesVersions 原本用于不同 TypeScript 版本使用不同类型声明,但常被用来处理子路径类型重定向。然而,当 moduleResolution 为 node16 及以上时,tsc 会优先使用 exports 中的 types 条件,忽略 typesVersions。因此,新库推荐直接用 exports 管理类型,无需再写 typesVersions;若需兼容旧策略,可保留 typesVersions 作为回退。
新库发布的最佳实践
文章建议新库只发布 ESM,设置 "type": "module",并使用 exports 定义入口和子路径。开发时通过 development 条件指向 src 源码以支持热更新,发布时用 publishConfig 覆盖为 dist 产物。同时,为兼容不支持 exports 的旧工具,可保留 main、module、typings 字段。types 条件应放在 require/import 之前,确保类型解析正确。
Q&A
TypeScript 5.0 新增的 bundler 模块解析策略解决了什么问题?
bundler 策略解决了 Node 不支持 exports 以及 node16/nodenext 强制要求相对路径写扩展名的问题,让开发者可以使用 exports 声明类型的同时,相对路径模块可以不写扩展名。
package.json 的 exports 字段中,子路径通配符是如何工作的?
exports 支持通配符 *,例如 "./*": "./lib/*.js"。解析时,用模块名替换左侧的 .,得到模式如 lodash/*,与模块 id 匹配得到 * 的值,再替换右侧目标模式中的 *,最终得到实际文件路径。
在 exports 中,条件导出的优先级是如何确定的?
条件导出的优先级取决于声明的顺序,越靠前的条件优先级越高,采用从前到后的短路匹配。因此应将更特殊的条件放在前面。
typesVersions 和 exports 中的 types 条件有什么区别?
typesVersions 主要用于根据 TypeScript 版本选择不同的类型声明,而 exports 中的 types 条件用于统一管理类型解析规则。在 node16/nodenext/bundler 等支持 exports 的策略下,tsc 会优先使用 exports 的 types 条件,忽略 typesVersions。
为什么 node16/nodenext 模块解析策略很少被使用?
因为 node16/nodenext 强制要求相对路径导入必须写扩展名,并且对于同时提供 ESM 和 CJS 的包,需要编写两套类型声明文件,使用起来非常繁琐,所以社区很少采用。
开发新的 npm 包时,推荐的模块解析最佳实践是什么?
推荐只发布 ESM 模块,设置 "type": "module",使用支持不写扩展名的打包工具(如 Vite/Rollup),TypeScript 版本 >=5.0 并设置 "moduleResolution": "bundler",在 package.json 中使用 exports 管理解析规则,并利用 publishConfig 在发布时覆盖 exports 配置。
exports 字段的优先级匹配算法是怎样的?
当多个 pattern 都能匹配时,选择最具体的 pattern。算法是:筛选所有匹配的 pattern,构造树,按层级遍历,优先取当前层非通配符的节点,最终到达叶子节点的路径即为最特殊的 pattern。
如何禁止某个子路径被导入?
可以将该子路径的 target pattern 设置为 null,例如 "exports": { "./forbidden": null },这样导入时会报错 ERR_PACKAGE_PATH_NOT_EXPORTED。