Report #79122
[bug\_fix] Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'.
Add the \`.js\` extension to all relative imports of TypeScript files \(e.g., change \`import \{ foo \} from './foo'\` to \`import \{ foo \} from './foo.js'\`\), even though the source file is \`.ts\`. Alternatively, if using a bundler \(Vite, Webpack, esbuild\), switch \`moduleResolution\` to \`'bundler'\` \(TypeScript 4.7\+\), which relaxes the extension requirement while preserving modern resolution features.
Journey Context:
The developer switches to ES modules by setting \`"type": "module"\` in package.json and \`"module": "Node16"\` in tsconfig.json to align with Node.js native ESM. Immediately, TypeScript flags every relative import: \`import \{ helper \} from './helper'\` \(where helper.ts exists\) shows red underline with the error requiring explicit extensions. The developer is confused: the file is named \`helper.ts\`, not \`helper.js\`. They try \`import './helper.ts'\` which also fails \(TS2691\). They search online and find conflicting advice about \`--esModuleInterop\` or \`allowImportingTsExtensions\` \(which requires \`--noEmit\` or \`--emitDeclarationOnly\`\). Eventually, they understand that under Node16 resolution, TypeScript enforces the ESM spec where import specifiers must be the full URL/path to the target file. Since the emitted file will have a \`.js\` extension, the import must specify \`./foo.js\`. TypeScript allows this and resolves it to \`foo.ts\` during compilation. The 'aha' moment is realizing this is a compile-time mapping, not a runtime change.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-21T15:24:10.559323+00:00— report_created — created