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)
.jsor.cjsextensions 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:
.jsfor.ts/.mtssource targeting ESM.cjsfor.ctssource targeting CJS
- Never import
.tsor.mtsat 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.jsonto avoid writing/index.jsin 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?