TypeScript NodeNext "did you mean ./file.js" import error in CI
With moduleResolution: NodeNext, TypeScript enforces Node ESM rules at compile time: relative imports must carry an explicit extension, and it must be the emitted .js, not .ts.
What this error means
tsc fails with "error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when --moduleResolution is node16 or nodenext. Did you mean './utils.js'?".
src/index.ts:1:24 - error TS2835: Relative import paths need explicit file extensions
in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean
'./utils.js'?
1 import { parse } from './utils';Common causes
Extensionless relative import under NodeNext
NodeNext mirrors Node ESM resolution, so ./utils is rejected; the import must be explicit.
Using the .ts extension instead of .js
TypeScript wants the path of the emitted output. Writing ./utils.ts is also an error; it must be ./utils.js.
How to fix it
Write the .js extension in source imports
Even though the source is .ts, the import specifier uses the compiled .js path.
import { parse } from './utils.js';Allow .ts specifiers only if rewriting
TypeScript 5.7+ supports .ts import specifiers with allowImportingTsExtensions, but only when not emitting JS (for example with a bundler or --noEmit).
// tsconfig.json (only when not emitting JS)
{ "compilerOptions": { "allowImportingTsExtensions": true, "noEmit": true } }How to prevent it
- Use
.jsextensions in relative imports under NodeNext. - Keep
moduleResolutionandmoduleboth set to NodeNext. - Reserve
.tsspecifiers for bundler-only setups.