Report #16130
[bug\_fix] Cannot find module './utils.js' or its corresponding type declarations when using \`.js\` extension in TypeScript import, or TS2691: An import path cannot end with a '.ts' extension
Set \`moduleResolution\` to \`"bundler"\` \(TypeScript 4.7\+\) when working with modern bundlers \(Vite, Webpack 5, esbuild, Rollup\) that handle extensionless imports or TypeScript-to-JavaScript path rewriting. Alternatively, if targeting Node.js native ESM without a bundler, use \`moduleResolution: "Node16"\` or \`"NodeNext"\` and you must use explicit \`.js\` extensions in your TypeScript source imports \(even for \`.ts\` files\) because Node.js ESM requires full specifiers; TypeScript 4.7\+ allows importing \`.js\` files that resolve to \`.ts\` source during type-checking. Root cause: The classic \`"node"\` module resolution algorithm predates ESM and assumes CommonJS-style extensionless lookups or directory index resolution, while modern tools and Node.js ESM have stricter rules about explicit file extensions.
Journey Context:
You're migrating your project to ES Modules \(ESM\) by adding \`"type": "module"\` to package.json and updating tsconfig.json to \`"module": "ESNext"\`. You read that Node.js ESM requires explicit file extensions, so you change an import from \`import \{ foo \} from './utils'\` to \`import \{ foo \} from './utils.js'\` \(even though the source file is \`utils.ts\`\). TypeScript immediately complains: "Cannot find module './utils.js' or its corresponding type declarations". You try \`import \{ foo \} from './utils.ts'\` but TypeScript throws "TS2691: An import path cannot end with a '.ts' extension". You're trapped in a paradox: Node.js runtime needs .js, TypeScript refuses .ts in imports, and omitting the extension breaks ESM semantics. You suspect it's a configuration issue and dive into \`tsconfig.json\`. You try changing \`moduleResolution\` from \`"node"\` to \`"node16"\` as suggested in a GitHub comment. Now TypeScript accepts the \`.js\` extension in the import and understands it maps to the \`.ts\` file, but now other packages are resolving strangely. Frustrated, you try \`"moduleResolution": "bundler"\` \(TypeScript 4.7\+\) since you're using Vite anyway. Instantly, the errors vanish. You can now use \`import \{ foo \} from './utils'\` without extensions again because the "bundler" resolution strategy understands that bundlers handle the extension resolution and don't require strict ESM specifiers in source. You realize that \`moduleResolution\` is the crucial lever that determines whether TypeScript enforces Node.js native ESM rules \(strict extensions, directory imports forbidden\) or modern bundler rules \(flexible resolution\).
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-17T01:52:29.506318+00:00— report_created — created