Report #22441
[bug\_fix] Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is set to 'bundler', 'node16', or 'nodenext'. Did you mean './utils.js'? \(TS2835\)
Add the \`.js\` extension to the import path \(e.g., \`import \{ foo \} from './utils.js'\`\), even though the source file is \`utils.ts\`. Root cause: TypeScript 4.7\+ enforces ESM specification compliance where import specifiers must include the file extension; since TypeScript emits \`.js\` files, the import in the source must anticipate the output extension.
Journey Context:
Developer upgrades their Node.js backend project to TypeScript 5.0 and decides to enable ES Modules by setting \`"type": "module"\` in package.json and \`"module": "NodeNext"\` and \`"moduleResolution": "NodeNext"\` in tsconfig.json to align with modern Node.js ESM requirements. They change all their \`require\(\)\` statements to \`import\` statements. Suddenly, every relative import like \`import \{ utils \} from './utils'\` throws TS2835, insisting they use \`./utils.js\`. The developer is baffled because \`utils.ts\` exists, not \`utils.js\`. They try \`./utils.ts\`, which satisfies the compiler but will fail at runtime because Node.js ESM doesn't automatically add extensions and the emitted file is \`.js\`. They check the TypeScript documentation and realize that when targeting ESM, TypeScript expects the import specifier to reflect the URL that will exist at runtime, which includes the \`.js\` extension. This feels counter-intuitive because they are writing TypeScript. The fix is to write \`import \{ utils \} from './utils.js'\`. TypeScript's module resolution logic, when set to \`NodeNext\`, will resolve \`./utils.js\` to \`./utils.ts\` during compilation, but emit \`./utils.js\` in the output, satisfying both the compiler and the Node.js ESM loader at runtime.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-17T16:04:54.559049+00:00— report_created — created