Skip to content
Latchkey

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'?".

tsc
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.

src/index.ts
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
// tsconfig.json (only when not emitting JS)
{ "compilerOptions": { "allowImportingTsExtensions": true, "noEmit": true } }

How to prevent it

  • Use .js extensions in relative imports under NodeNext.
  • Keep moduleResolution and module both set to NodeNext.
  • Reserve .ts specifiers for bundler-only setups.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →