Report #54625
[bug\_fix] Relative import paths need explicit file extensions in ECMAScript imports. Did you mean './utils.js'?
Add \`.js\` extensions to all relative imports in TypeScript source files \(e.g., \`import \{ foo \} from './utils.js'\`\), even though you're importing \`.ts\` files. Root cause: When TypeScript targets ESM \(\`module: nodenext/node16/esnext\`\) with modern Node.js resolution, it adheres to the ES Module specification which requires complete URLs including extensions. TypeScript does not rewrite import paths during compilation \(it preserves the specifier\), so the \`.js\` extension in the source maps directly to the compiled \`.js\` file on disk.
Journey Context:
You've decided to modernize your Node.js library to pure ESM \(\`type: module\` in package.json\). You set \`module: nodenext\` and \`moduleResolution: nodenext\` in tsconfig.json. Immediately, TypeScript starts complaining about your imports: 'Relative import paths need explicit file extensions'. You look at your code: \`import \{ helper \} from './helper'\`. You think 'But helper.ts is a TypeScript file, why would I add .js?' You try adding \`.ts\` extension, but TypeScript errors: 'An import path cannot end with a .ts extension'. You search online and find GitHub issues explaining that TypeScript requires \`.js\` extensions in source files when targeting ESM because TypeScript doesn't rewrite imports. You grudgingly change all imports to use \`.js\` extensions. You compile and run \`node dist/index.js\` and it works. You realize this is because the compiled JS files have \`.js\` extensions, and Node.js ESM resolution requires the specifier to match the actual file on disk, unlike CommonJS which allows extensionless lookups.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-19T22:10:59.873670+00:00— report_created — created