Skip to content

Behavior difference: declaration emit synthesizes extensionless import() specifier under allowImportingTsExtensions, invalid in nodenext #4616

Description

@dario-causaly

When a project uses allowImportingTsExtensions-style relative imports (import { x } from './helper.ts'), tsgo's declaration emit prints synthesized import() type references to package subpaths without their file extension. Under --module nodenext the extensionless specifier does not resolve, so the emitted .d.ts is invalid. Emit succeeds with no diagnostic.

Reproduced with typescript@7.0.2 and nightly @typescript/native-preview@7.0.0-dev.20260707.2.

Steps to reproduce

Self-contained — no npm install; the dependency is materialized by the script (a plain CJS package with no exports map, imported via an explicit-extension subpath, like fp-ts among others):

mkdir repro && cd repro && mkdir -p src node_modules/mylib/lib

cat > package.json <<'EOF'
{ "name": "repro", "type": "module" }
EOF

cat > tsconfig.json <<'EOF'
{
  "compilerOptions": {
    "module": "nodenext",
    "declaration": true,
    "emitDeclarationOnly": true,
    "allowImportingTsExtensions": true,
    "rootDir": "src",
    "outDir": "out"
  },
  "include": ["src"]
}
EOF

cat > node_modules/mylib/package.json <<'EOF'
{ "name": "mylib", "version": "1.0.0", "main": "./lib/index.js" }
EOF

cat > node_modules/mylib/lib/Box.js <<'EOF'
exports.box = (value) => ({ value });
EOF

cat > node_modules/mylib/lib/Box.d.ts <<'EOF'
export interface Box<T> { readonly value: T }
export declare function box<T>(value: T): Box<T>;
EOF

cat > src/helper.ts <<'EOF'
import * as B from 'mylib/lib/Box.js';
export const inner = B.box('hello');
EOF

cat > src/mod.ts <<'EOF'
import { inner } from './helper.ts';
export const out = inner;
EOF

Then compile with each compiler and inspect out/mod.d.ts, as shown in the two sections below.

Behavior with typescript@6.0

npx -y -p typescript@6.0.2 tsc -p tsconfig.json
cat out/mod.d.ts

Emits a resolvable specifier:

export declare const out: import("mylib/lib/Box.js").Box<string>;

Behavior with tsgo

npx -y -p typescript@7.0.2 tsc -p tsconfig.json
cat out/mod.d.ts

The extension is dropped:

export declare const out: import("mylib/lib/Box").Box<string>;

The trigger is the .ts-extension relative import

One-character bisect: change src/mod.ts to import './helper.js' instead of './helper.ts' and tsgo emits the extension correctly. Generics, type aliases, and rewriteRelativeImportExtensions were all eliminated as factors.

The emitted file is invalid

cat > tsconfig.verify.json <<'EOF'
{
  "compilerOptions": { "module": "nodenext", "noEmit": true, "skipLibCheck": false },
  "include": ["out/mod.d.ts"]
}
EOF
npx -y -p typescript@7.0.2 tsc -p tsconfig.verify.json
out/mod.d.ts(1,34): error TS2307: Cannot find module 'mylib/lib/Box' or its corresponding type declarations.

(typescript@6.0.2 rejects it identically)

In a monorepo with skipLibCheck: true this is silent — the broken reference degrades to an error type and surfaces as unrelated-looking inference failures ('x' is of type 'unknown') in downstream packages.


Disclosure: this bug was found, minimized, and verified interactively by a human (me) working with Claude Code; the write-up was drafted by the model and reviewed by me. All commands and outputs above were executed and confirmed against the stated versions.

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions