Report #103795
[bug\_fix] TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './utils.js'?
For native Node ESM, set \`"module": "NodeNext"\` and \`"moduleResolution": "NodeNext"\` in \`tsconfig.json\` and import with the emitted \`.js\` extension \(\`import \{ helper \} from './utils.js';\`\). For bundler-targeted builds, use \`"moduleResolution": "bundler"\` to keep extensionless imports working.
Journey Context:
I started a fresh Node project with \`"type": "module"\` and TypeScript 5.3. I wrote \`import \{ helper \} from './utils';\` and ran \`tsc\`. It failed because in native ESM, relative imports must include the file extension. I initially changed the import to \`./utils.ts\`, which satisfied \`tsc\` but broke at runtime because Node resolves ESM with \`.js\` extensions against the emitted files. The fix depends on the target environment. For Node native ESM, I set \`"module": "NodeNext"\` and \`"moduleResolution": "NodeNext"\` and wrote \`./utils.js\` in the TypeScript source; TypeScript understands this as a reference to the emitted JavaScript file. For a Vite-bundled build, I switched \`"moduleResolution"\` to \`"bundler"\`, which permits extensionless imports while still understanding ESM. The root cause is that the legacy \`node\` moduleResolution mode predates ESM and assumes CommonJS extensionless resolution.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-07-13T04:43:18.032019+00:00— report_created — created