File extensions for module imports
1. Node.js ESM / CJS rules
File being imported | Node module type | Import syntax | Extension required? | Notes |
---|---|---|---|---|
.js | ESM | import | ✅ Yes | Relative ESM imports must include .js . Directory imports are not allowed. |
.mjs | ESM | import | ✅ Yes | Explicit .mjs is required if you’re targeting ESM in Node. |
.cjs | CJS | require | ✅ Optional | Node can resolve .cjs without extension. In ESM, you must use .cjs if you import . |
.ts (source) | N/A | import (TS source) | ❌ Do not use .ts at runtime | TS compiler resolves .js specifiers back to .ts source. Runtime only sees .js . |
.mts (TS ESM) | ESM | import | ✅ Yes (use .js in runtime) | Source file extension .mts is TypeScript-only; runtime uses .js . |
.cts (TS CJS) | CJS | require | ✅ Optional | Same as above; TS source, runtime is .js . |
Key rules by context
1. Relative imports in ESM (Node)
- Always include explicit file extension:
.js
/.mjs
/.cjs
. - Example:
import { helper } from "./utils/helper.js"; // ✅
- Directory imports do not work:
import { helper } from "./utils"; // ❌ ERR_UNSUPPORTED_DIR_IMPORT
2. Relative imports in CJS (Node)
.js
or.cjs
extensions are optional. Node will resolve:./module.js ./module.cjs ./module/index.js ./module/index.cjs
- Example:
const helper = require("./utils"); // ✅ Node will find ./utils/index.js
3. TypeScript source files (.ts
, .mts
, .cts
)
- Always write runtime-compatible extensions (
.js
) in import specifiers, even if importing.ts
.import { helper } from "./utils/helper.js"; // ✅ TS resolves this to ./utils/helper.ts
- Node never sees
.ts
,.mts
,.cts
. These are compiled away.
4. Dynamic imports
- Same rule: use the runtime extension (
.js
) in the specifier. - Example:
const mod = await import("./feature.js");
2. When to use which extension
Import type | Use in source | Runtime seen by Node |
---|---|---|
.ts | Never in import | Node doesn’t see .ts |
.mts | Source file | Compiled to .js at runtime |
.cts | Source file | Compiled to .js at runtime |
.js | Runtime file | Node sees .js |
.cjs | Runtime file | Node sees .cjs |
.mjs | Runtime file | Node sees .mjs |
3. Summary guidelines
- Always write the runtime extension in TypeScript source imports:
.js
for.ts/.mts
source targeting ESM.cjs
for.cts
source targeting CJS
- Never import
.ts
or.mts
at runtime. Node cannot run them. - Directory imports only work in CJS, not in ESM. For ESM, always point to
/index.js
. - Dynamic imports follow the same rule: use runtime extension.
- Use
"exports"
or"imports"
inpackage.json
to avoid writing/index.js
in public APIs.
If you want, I can make a visual map showing all 6 file extensions, what TypeScript sees, and what Node sees, with arrows showing what extension to use in each import. It’s a really handy cheat sheet. Do you want me to do that?