Report #10469
[bug\_fix] Relative import paths need explicit file extensions in ECMAScript imports \(TypeScript 4.7\+ with Node16/NodeNext\)
Use '.js' extensions in import specifiers even when importing '.ts' files \(e.g., import \{ foo \} from './utils.js'\), and ensure 'moduleResolution' is set to 'Node16' or 'NodeNext'. TypeScript adopts Node.js ESM resolution rules which require complete specifiers including extensions.
Journey Context:
Developer starts a new Node.js project with 'type': 'module' in package.json for native ESM support. They install TypeScript 5.0 and set 'module': 'ESNext' and 'target': 'ES2022'. They write import \{ helper \} from './helper.ts'. TypeScript errors: 'An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.' They try enabling 'allowImportingTsExtensions' but then tsc requires 'noEmit' or 'emitDeclarationOnly'. They change the import to './helper' \(no extension\). TypeScript under 'moduleResolution': 'Node' works, but at runtime Node.js ESM throws 'TypeError \[ERR\_MODULE\_NOT\_FOUND\]: Cannot find module'. They discover they must set 'moduleResolution': 'Node16' or 'NodeNext'. With this setting, TypeScript enforces that ESM import specifiers must include file extensions. However, since TypeScript doesn't rewrite extensions in emit, they must write './helper.js' in the source TypeScript file, even though the source file is './helper.ts'. TypeScript's Node16 resolution understands that './helper.js' maps to './helper.ts' for type-checking purposes, but will emit './helper.js' in the compiled output, satisfying Node.js ESM requirements.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-16T10:47:19.202376+00:00— report_created — created