第6章解决“怎样配置一个项目”,本章进一步回答“为什么模块配置必须这样选”。核心原则:TypeScript 不是模块宿主,它必须模拟 Node.js、浏览器或 bundler 的真实加载规则。
没有顶层 import/export 的传统脚本共享全局作用域;模块拥有独立作用域,并显式公开能力。
// 模块 export const version = "1.0";
必要时可以用空导出强制把文件标记为模块:
“宿主”是最终加载代码并解析模块说明符的系统:
TypeScript 要完成三项工作:
两者必须匹配真实宿主。即使 noEmit: true,module 仍影响模块格式识别、互操作规则、import.meta 和顶层 await 等检查。
Node.js 根据扩展名和最近的 package.json 判断格式:
因此 Node 应用通常选择 node16、node18、node20 或 nodenext 这一类能够模拟 Node 规则的设置,而不是简单写 module: esnext 就认为足够。
TypeScript 默认不会替你把错误路径变成宿主可加载路径。Node ESM 的相对导入通常要求输出路径带扩展名,因此源码中常写对应输出的 .js 扩展名。
paths 也主要帮助 TypeScript 解析,不会自动改写输出说明符。
常见难点:
esModuleInterop 与 allowSyntheticDefaultImports 改善部分兼容体验,但不会统一所有宿主行为。应用应选择与运行环境匹配的配置;库作者需要测试真实消费方式。
通常让 TypeScript 模拟 bundler 解析,并由 bundler 负责输出。类型检查独立运行:
配置必须模拟目标 Node 版本,并同步检查:
导入必须是浏览器或 Import Map 能解析的 URL/说明符,不能依赖 Node 的包解析幻想。
库作者至少要决定:
示意:
双格式方案更复杂,不应复制模板后直接发布,必须建立 ESM、CJS 和 bundler 消费测试。
TypeScript 查找运行文件时,会尝试关联声明文件或源码类型。例如运行时解析到 .js,类型系统可能对应查找 .d.ts。这要求声明文件的模块格式与对应 JavaScript 一致,否则会出现“类型认为能导入、运行时却失败”。
当出现“编辑器正常、运行时报模块错误”时,按顺序检查:
请继续阅读:第13章:声明文件与库发布。