基础定义

JavaScript 原生后缀(Node.js 层面)

Node.js 提供三种原生 JS 文件后缀,对应不同的模块系统:

后缀模块系统对应 TS 后缀Node.js 默认行为
.js跟随 package.json 的 type 字段配置.ts"type": "module" 时为 ESM,否则为 CJS
.mjs强制 ES Module (ESM).mts无论 package.json 配置如何,始终作为 ESM 加载
.cjs强制 CommonJS (CJS).cts无论 package.json 配置如何,始终作为 CommonJS 加载

TypeScript 扩展后缀

TypeScript 在 JS 基础上提供对应的类型文件后缀:

后缀模块系统编译输出Node.js 默认行为
.ts跟随 tsconfig.json 配置的 module 选项.js根据 package.json 的 type 字段决定
.mts强制 ES Module (ESM).mjs始终作为 ES Module 加载
.cts强制 CommonJS (CJS).cjs始终作为 CommonJS 加载

核心区别与使用场景

JavaScript 后缀详解

1. .js - 默认 JS 后缀

  • 适用场景:绝大多数普通 JS 项目、不需要明确区分模块系统的代码
  • 行为特点:
    • 模块系统由 package.json 的 type 字段决定:
      • "type": "module" → 按 ESM 加载
      • 无 type 字段或 "type": "commonjs" → 按 CJS 加载
    • 最灵活,适合大部分开发场景

2. .mjs - 强制 ESM 后缀

  • 适用场景:
    • 需要明确使用 ES Module 的 Node.js 库/工具
    • 在 CJS 为主的项目中需要使用 ESM 语法的单个文件
    • 双发布包的 ESM 入口
  • 行为特点:
    • 始终按 ESM 加载,不受 package.json type 字段影响
    • 支持 import/export、import.meta、顶层 await 等 ESM 特性
    • 不支持 require()、module.exports、__dirname 等 CJS 特性
    • 文件必须使用 ESM 语法

3. .cjs - 强制 CJS 后缀

  • 适用场景:
    • 需要明确使用 CommonJS 的 Node.js 库/工具
    • 在 ESM 为主的项目中需要使用 CJS 语法的单个文件
    • 双发布包的 CJS 入口
    • 与旧版 Node.js 项目兼容的代码
  • 行为特点:
    • 始终按 CJS 加载,不受 package.json type 字段影响
    • 支持 require()、module.exports、__dirname、__filename 等 CJS 特性
    • 不支持顶层 await、import.meta 等 ESM 特性
    • 文件必须使用 CJS 语法

TypeScript 后缀详解

1. .ts - 默认 TS 后缀

  • 适用场景:绝大多数普通项目、前端项目、不需要明确区分模块系统的代码
  • 行为特点:
    • 编译输出格式完全由 tsconfig.json 的 module 和 moduleResolution 决定
    • 在 Node.js 环境中,若 package.json 包含 "type": "module" 则按 ESM 加载,否则按 CJS 加载
    • 最灵活,适合大部分开发场景,无需刻意指定后缀

2. .mts - 强制 ES Module

  • 适用场景:
    • 需要明确使用 ES Module 的 Node.js 库/工具
    • 同时支持 ESM 和 CJS 的双发布包的 ESM 入口
    • 使用 import/export 语法且需要和 .mjs 文件互操作的场景
  • 行为特点:
    • 编译后固定输出 .mjs 文件
    • 无论 package.json 配置如何,Node.js 始终按 ESM 加载
    • 可以直接使用 import.meta、顶层 await 等 ESM 专属特性
    • 不支持 require()、module.exports 等 CommonJS 语法

3. .cts - 强制 CommonJS

  • 适用场景:
    • 需要明确使用 CommonJS 的 Node.js 库/工具
    • 同时支持 ESM 和 CJS 的双发布包的 CJS 入口
    • 与旧版 Node.js 项目兼容的代码
  • 行为特点:
    • 编译后固定输出 .cjs 文件
    • 无论 package.json 配置如何,Node.js 始终按 CommonJS 加载
    • 支持 require()、module.exports、__dirname、__filename 等 CommonJS 专属特性
    • 不支持顶层 await

编译配置说明

tsconfig.json 关键配置

{
  "compilerOptions": {
    // 输出模块格式,.mts/.cts 会忽略此配置强制对应格式
    "module": "NodeNext", // 或 ESNext / CommonJS
    "moduleResolution": "NodeNext", // 推荐用于混合模块场景
    "target": "ES2022",
    
    // 输出目录
    "outDir": "./dist",
    
    // 类型声明输出
    "declaration": true,
    "declarationMap": true
  }
}

双模块发布最佳实践

对于需要同时支持 ESM 和 CJS 的 npm 包,推荐目录结构:

src/
├── index.mts          # ESM 入口
├── index.cts          # CJS 入口
└── core/
    ├── shared.ts      # 通用代码
    ├── esm-utils.mts  # ESM 专属工具
    └── cjs-utils.cts  # CJS 专属工具

对应的 package.json 配置:

{
  "name": "your-package",
  "version": "1.0.0",
  "type": "module", // 包默认使用 ESM
  
  // 入口配置
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  },
  
  // 兼容旧版 Node.js
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts"
}

常见问题与注意事项

1. 模块导入导出匹配

  • 在 .mts 文件中只能 import 其他 ESM 文件(.mts/.ts 当配置为 ESM 时)
  • 在 .cts 文件中只能 require 其他 CommonJS 文件(.cts/.ts 当配置为 CJS 时)
  • 若要在 ESM 中导入 CJS 模块,需要使用 import module from 'cjs-module' 语法,且该模块需支持默认导出

2. 类型声明文件

  • .mts 编译后会生成对应的 .d.mts 类型声明文件
  • .cts 编译后会生成对应的 .d.cts 类型声明文件
  • TypeScript 会自动处理不同后缀的类型导入导出匹配

3. 工具链兼容性

  • 大部分现代打包工具(Vite、Rollup、Webpack 5+)都已支持 .mts/.cts 后缀
  • Node.js 14.13.0+ 开始原生支持 .mjs/.cjs,对应 TypeScript 4.5+ 开始支持 .mts/.cts
  • 旧版工具可能需要额外配置才能识别新后缀

4. 什么时候不需要用特殊后缀?

  • 纯前端项目(浏览器环境)不需要区分,统一用 .ts/.js 即可
  • 只针对单一模块系统的项目,无需刻意使用 .mts/.cts/.mjs/.cjs
  • 使用 ts-node、bun、deno 等运行时,默认配置下都能正确处理 .ts/.js 文件的模块系统

相关链接

  • tsconfig-json — tsconfig.json 配置详解
  • swc — Rust 实现的 TypeScript 编译器,支持所有后缀格式