Behavior difference: import * as x of an export = callable loses its call signatures without esModuleInterop (TS2349)
🔎 Search terms
import star, export equals, export =, namespace import, not callable, TS2349, esModuleInterop, allowSyntheticDefaultImports, synthetic default, getSpreadType, resolveESModuleSymbol
🕗 Version & Regression Information
- Reproduces on
7.0.0-dev.20260707.2 (and every earlier nightly tried).
- Behavior differs from
tsc (TypeScript 5.7).
⏯ Playground / Repro
tsc accepts, tsgo reports TS2349. No esModuleInterop.
mod.d.ts
declare const thing: {
(x: string): number;
prop: boolean;
};
export = thing;
use.ts
import * as thing from './mod';
const a: number = thing('hello'); // tsgo: TS2349 — This expression is not callable
const b: boolean = thing.prop;
tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"allowSyntheticDefaultImports": true,
"strict": false,
"noEmit": true
},
"include": ["use.ts"]
}
🙁 Actual behavior (tsgo)
use.ts(2,19): error TS2349: This expression is not callable.
Type '{ prop: boolean; default: { (x: string): number; prop: boolean; } }' has no call signatures.
tsgo synthesises a namespace type { prop; default } for the import * as
binding — it hoists the export = value to .default, spreads its properties,
and drops the call/construct signatures.
🙂 Expected behavior (tsc)
tsc produces no error — thing keeps its call signature and is callable. Only
when esModuleInterop: true (or an ESM→CJS reference) does tsc emit the same
TS2349. Real-world impact: any import * as request from 'supertest' /
import * as archiver from 'archiver' followed by request(...) / archiver(...)
fails under tsgo but is accepted by tsc — hundreds of errors across a normal
NestJS/e2e test suite that runs module: commonjs without esModuleInterop.
🌳 Root cause
resolveESModuleSymbol (internal/checker/checker.go) gates the synthetic-default
wrap on hasSignatures(typ) || hasDefault || isEsmCjsRef. tsc's
resolveESModuleSymbol gates it on getESModuleInterop(compilerOptions) || isEsmCjsRef
and only then checks for signatures/default. tsgo dropped the esModuleInterop
guard, so a callable export = is wrapped (and its signatures spread away by
getSpreadType) even when interop is off.
There is also no getESModuleInterop equivalent on CompilerOptions in tsgo, so
the implicit-true cases (module: node16/18/20/nodenext/preserve) aren't accounted
for at this call site.
✅ Suggested fix
Add CompilerOptions.GetESModuleInterop() mirroring tsc (explicit option, else
true for node16/18/20/nodenext/preserve), and gate the wrap on
GetESModuleInterop() || isEsmCjsRef with the hasSignatures || hasDefault check
moved inside. Verified against the local conformance suite (TestLocal) — passes,
with one cosmetic symbol-baseline shift (globalArrayAugmentation…:
foo.items→items, which is the un-wrapped, tsc-correct form). PR to follow.
Behavior difference:
import * as xof anexport =callable loses its call signatures without esModuleInterop (TS2349)🔎 Search terms
import star, export equals, export =, namespace import, not callable, TS2349, esModuleInterop, allowSyntheticDefaultImports, synthetic default, getSpreadType, resolveESModuleSymbol
🕗 Version & Regression Information
7.0.0-dev.20260707.2(and every earlier nightly tried).tsc(TypeScript 5.7).⏯ Playground / Repro
tscaccepts,tsgoreports TS2349. NoesModuleInterop.mod.d.tsuse.tstsconfig.json{ "compilerOptions": { "module": "commonjs", "allowSyntheticDefaultImports": true, "strict": false, "noEmit": true }, "include": ["use.ts"] }🙁 Actual behavior (tsgo)
tsgosynthesises a namespace type{ prop; default }for theimport * asbinding — it hoists the
export =value to.default, spreads its properties,and drops the call/construct signatures.
🙂 Expected behavior (tsc)
tscproduces no error —thingkeeps its call signature and is callable. Onlywhen
esModuleInterop: true(or an ESM→CJS reference) doestscemit the sameTS2349. Real-world impact: any
import * as request from 'supertest'/import * as archiver from 'archiver'followed byrequest(...)/archiver(...)fails under tsgo but is accepted by tsc — hundreds of errors across a normal
NestJS/e2e test suite that runs
module: commonjswithoutesModuleInterop.🌳 Root cause
resolveESModuleSymbol(internal/checker/checker.go) gates the synthetic-defaultwrap on
hasSignatures(typ) || hasDefault || isEsmCjsRef.tsc'sresolveESModuleSymbolgates it ongetESModuleInterop(compilerOptions) || isEsmCjsRefand only then checks for signatures/default. tsgo dropped the
esModuleInteropguard, so a callable
export =is wrapped (and its signatures spread away bygetSpreadType) even when interop is off.There is also no
getESModuleInteropequivalent onCompilerOptionsin tsgo, sothe implicit-true cases (
module: node16/18/20/nodenext/preserve) aren't accountedfor at this call site.
✅ Suggested fix
Add
CompilerOptions.GetESModuleInterop()mirroringtsc(explicit option, elsetruefor node16/18/20/nodenext/preserve), and gate the wrap onGetESModuleInterop() || isEsmCjsRefwith thehasSignatures || hasDefaultcheckmoved inside. Verified against the local conformance suite (
TestLocal) — passes,with one cosmetic symbol-baseline shift (
globalArrayAugmentation…:foo.items→items, which is the un-wrapped, tsc-correct form). PR to follow.