为何export *与逐个导出在CommonJS下会导致Jest测试配置差异?
export *在CommonJS模块下会导致Jest spyOn失败,而逐个导出不会? 问题场景
在使用Jest测试时遇到报错:
TypeError: Cannot redefine property: getCurrentRouteSnapshot
原本在index.ts中使用export * from './router.tools'批量导出成员,测试用例中无法用spyOn监听这些导出的函数;改成逐个明确导出后,测试恢复正常。对应的测试环境TypeScript配置(tsconfig.spec.json)中module被设置为CommonJS。
根本原因分析
问题核心在于TypeScript对两种导出方式编译为CommonJS代码时的行为差异:
1. export *的编译逻辑
当使用export *时,TypeScript会把目标模块(./router.tools)的整个导出对象直接合并到当前模块的exports上,编译后的JS代码类似:
const routerTools = require('./router.tools'); Object.assign(exports, routerTools);
由于CommonJS模块导出的属性默认是**不可配置(non-configurable)**的(Node.js模块系统的固有特性),通过Object.assign复制过来的属性会继承这个特性——也就是说,这些属性的configurable描述符为false。
而Jest的spyOn本质是通过修改属性的描述符(比如替换成带监听逻辑的getter/setter)来实现的,当属性不可配置时,就会抛出Cannot redefine property的错误。
同时,这种场景下TypeScript不会给当前模块的exports添加__esModule: true标记,进一步导致ES模块与CommonJS模块交互时的属性配置行为不符合预期。
2. 逐个导出的编译逻辑
当明确逐个导出成员时,TypeScript会为每个成员生成单独的赋值语句,编译后的JS代码类似:
const routerTools = require('./router.tools'); exports.getCurrentRouteSnapshot = routerTools.getCurrentRouteSnapshot; exports.GetCurrentApplication = routerTools.GetCurrentApplication; // ... 其他成员
这种直接赋值的方式添加到exports上的属性,默认是**可配置(configurable)**的——Jest的spyOn可以正常修改这些属性的描述符,因此不会报错。
另外,TypeScript会为这种导出方式的模块添加__esModule: true标记,确保ES模块的导入行为符合预期,同时也间接保证了导出属性的可配置性。
总结
export *在CommonJS编译目标下会复制不可配置的属性到当前模块导出,导致Jest无法修改属性实现监听;- 逐个导出会生成可配置的属性赋值,让Jest的
spyOn可以正常工作。
内容的提问来源于stack exchange,提问作者IMeyers20

