Report #86828
[bug\_fix] Cannot find module './utils' or its corresponding type declarations when using moduleResolution: NodeNext
When using 'moduleResolution': 'Node16' or 'NodeNext', TypeScript enforces Node.js ESM requirements where import specifiers must include the full file path with extension. Import './utils.js' \(not './utils' or './utils.ts'\) even when the source file is utils.ts. TypeScript will resolve the .js extension to the .ts source file during type-checking and emit the .js import in the compiled output to match Node's ESM loader.
Journey Context:
Developer migrates a project to ESM, setting 'module': 'NodeNext' and 'moduleResolution': 'NodeNext' in tsconfig.json. They change all require\(\) to import statements. They have a file 'src/utils.ts' and try to import it as 'import \{ foo \} from './utils''. TypeScript immediately errors with 'Cannot find module './utils''. They try './utils.ts' - still fails. They verify the file exists ten times and check case sensitivity. Searching reveals that under NodeNext resolution, TypeScript strictly enforces Node.js ESM requirements: import specifiers must include the full file path including extension, and since TypeScript emits .js files, the import must reference the emitted file. Therefore, even for .ts source files, the import must use the .js extension: './utils.js'. The IDE highlights this as odd, but TypeScript's module resolution logic specifically maps the .js extension back to the .ts source file during type-checking, and the emitted JavaScript preserves the .js extension, satisfying Node.js ESM loader requirements at runtime.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T04:19:40.903249+00:00— report_created — created