Report #4732
[bug\_fix] Relative import paths need explicit file extensions in ECMAScript imports \(TS2835\) or Module resolution failure with ESNext
Set 'module': 'NodeNext' \(or 'Node16'\) and 'moduleResolution': 'NodeNext' together in tsconfig.json. Update all relative imports to use explicit file extensions \(e.g., './utils.js' instead of './utils'\), even when importing TypeScript files, as Node.js ESM requires the extension and TypeScript will map it correctly during compilation. Root cause: When targeting ESM for Node.js, the 'NodeNext' module resolution mode enforces the same rules as Node.js native ESM: mandatory file extensions, import.meta.url availability, and strict resolution. Using 'module': 'ESNext' with 'moduleResolution': 'node' \(classic\) creates a mismatch where TypeScript assumes a bundler will handle resolution, but Node.js runtime fails.
Journey Context:
Developer creates a new Node.js project, sets 'type': 'module' in package.json to use native ESM. They write 'import \{ foo \} from './foo';' \(no extension\) and TS complains 'Relative import paths need explicit file extensions in ECMAScript imports. Did you mean './foo.js'?'. They think 'but the file is .ts, why add .js?' They try changing tsconfig to 'module': 'ESNext', which allows the syntax but then Node.js runtime fails because it can't resolve the module without the extension. They search and find conflicting advice about using 'ts-node/esm' loaders. Finally, they discover that setting 'module': 'NodeNext' forces them to write './foo.js' in the import, which TypeScript understands as referring to './foo.ts' during compilation, and Node.js finds './foo.js' \(after compilation\) or uses the loader correctly. This aligns the entire pipeline.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T19:59:41.517092+00:00— report_created — created