CommonJS模式TS项目如何配合@types/node@16.x使用cluster模块
问题背景
我维护了一款基于Node.js cluster API开发的TypeScript应用,在Node 14.x版本搭配对应版本DefinitelyTyped提供的cluster类型定义时,项目可正常构建。
迁移到Node 16.x版本时,我将@types/node从14.x升级到16.x版本后,TypeScript出现编译失败:
- 查看
cluster对应的.d.ts类型文件内容与顶部文档说明,该类型要求使用默认导入写法:import cluster from 'cluster'; - 但我现有代码全部使用非默认的命名空间导入写法:
import * as cluster from 'cluster';
调整导入写法时出现两种矛盾的异常情况:
- 如果修改为默认导入写法,TypeScript可编译通过,但应用运行时抛出异常
- 如果保留原有
import * as ...的命名空间导入写法,需要在大量代码位置添加// @ts-expect-error注释才能完成编译,且编译后应用可正常运行
当前项目仍使用CommonJS规范的TypeScript配置,tsconfig中设置了"module": "commonjs", "moduleResolution": "node";由于项目体量较大,如果开启"esModuleInterop": true配置,会引发大量第三方依赖的类型报错。该问题可通过cluster.d.ts文件顶部的示例复现。
疑问解答
1. 关于使用方式正确性与cluster模块单独异常的原因
你的使用方式没有错误。
只有cluster出现该问题的核心原因是:Node 16对应的@types/node版本中,cluster模块的类型定义单独采用了仅支持ESM默认导出的写法,和其他Node内置模块的类型定义逻辑不一致——其他内置模块(如process、os)的类型都兼容无esModuleInterop场景下的命名空间导入写法,只有cluster的类型定义没有做CommonJS场景的兼容。
而Node.js运行时cluster本质仍是CommonJS模块,导出逻辑为module.exports = cluster实例,你用import * as cluster from 'cluster'拿到的才是符合运行时实际结构的模块对象;如果改成默认导入,在关闭esModuleInterop的CommonJS编译模式下,TS会把代码编译为const cluster = require('cluster').default,但实际cluster模块根本没有default属性,自然会出现运行时异常。
2. 迁移ESM前的临时解决方案
不需要在业务代码里到处加// @ts-expect-error注释,有侵入性极低的干净方案:
在项目全局生效的类型声明文件(通常是项目根目录下的global.d.ts,需要确保该文件被tsconfig的include配置覆盖)中,补充如下模块声明即可:
declare module 'cluster' { const cluster: import('cluster').default; export = cluster; }
补充这段声明后,原有import * as cluster from 'cluster'的写法就能正常通过类型校验,不需要修改业务代码、不需要调整tsconfig配置,也不会对第三方依赖的类型产生影响。
3. 关于类型定义本身的问题
该问题确实是DefinitelyTyped仓库中@types/node的类型定义bug,本质是类型声明和Node.js实际的CommonJS模块运行逻辑不匹配,已经有不少开发者反馈过该兼容问题,后续高版本的@types/node已经修复了该问题。如果暂时不方便升级@types/node版本,使用上面提到的补充类型声明的方案即可,不需要到处加错误忽略注释。
内容的提问来源于stack exchange,提问作者Ronan Jouchet

