TypeScript: This expression is not callable for ESM consuming CJS with default export
Bug Report
The package react-use-websocket is written in TypeScript and transpiled by TypeScript. It transpiles to CJS and exposes a default export.
When consume by a ESM project, TS errors out: This expression is not callable.
đ Search Terms
ESM CJS default export
đ Version & Regression Information
4.9.3 (likely from 4.7.0)
5.0.0-dev.20230103 also fails
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about ESM CJS
⯠Playground Link
Repro link: https://github.com/cyberuni/ts-esm-on-cjs-with-default-export
đ» Code
import useWebSocket from 'react-use-websocket'
// or import { default as useWebSocket } from 'react-use-websocket'
// or import * as ws from 'react-use-websocket'; ws.default(...)
useWebSocket('abc') // <-- This expression is not callable
tsconfig.json:
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"module": "Node16", // doesn't matter
"moduleResolution": "Node16",
"outDir": "dist",
"strict": true,
"target": "ES2019"
},
"include": [
"ts",
"types"
]
}
đ Actual behavior
This expression is not callable
đ Expected behavior
work
About this issue
- Original URL
- State: closed
- Created a year ago
- Reactions: 6
- Comments: 19 (14 by maintainers)
https://arethetypeswrong.github.io/?p=%40streetstrider%2Femitter%401.2.1
The problem is, that is exactly what
tsc
generates when compiling a default export. I have created https://github.com/cyberuni/typescript-module-resolutions-demo to demonstrate howtsc
behaves.IMO it is just wrong (or put in another way, some bugs through the workflow), that TypeScript cannot consume code generated by TypeScript.
btw, I just realized one thing, to specify a different typings for CJS when using the
exports
map, it needs to be done like this:Unless you keep the
main
andtypes|typings
field at the top-level. i.e. a hybrid approach:Again I think we broadly agree. There are definitely places where you want to ship both CJS and ESM. No contention there. Itâs also possible (though of course by no means trivial) to write a tool which can take a module written in one module system and, with proper configuration, produce a reasonably-similar module in the other system, subject to some constraints which can be met in most cases.
What Iâm saying is, the tool that does this transformation is not TypeScript, or at least should not be. The CJS <-> ESM module problem is not one that TypeScript can uniquely solve, nor is it one that can only be solved in a TypeScript codebase, nor is it one that is currently unsolved. Itâs a problem on the same tier as minification, downleveling, linting, bundling, gzipping, tree-shaking, and so on: those which the JavaScript ecosystem broadly needs, has, and is in no want of an N+1th solution. Weâre trying to spend as much time as we can writing the worldâs best static type system for JavaScript and not writing the worldâs 43rd tool concerned with doing machine translation of JavaScript from one format to another.
Thatâs what âpick one runtime targetâ means â not that you canât or shouldnât dual-ship CJS/ESM, but rather than you should go TS (A) -> JS (A) -> JS(A+ B) where A is either CJS or ESM, B is the other one, and the tool doing the second arrow is some tool that isnât TypeScript. If there are things preventing that, those are the things that we want to hear about, not that half-baked mostly-non-working solutions derived from trying to make ill-advised TS (A) -> JS (B) transforms work when really, allowing that transformation in the first place was a really bad idea that we shouldnât have implemented in the first place.
Yes, I actually agree. This bug corresponding to the
node16 | cjs
case in https://github.com/cyberuni/typescript-module-resolutions-demo/blob/main/tests/node16/test-result.4.9.4.md which are working as expected.Will do as Iâm going through the cases in https://github.com/cyberuni/typescript-module-resolutions-demo It will take me some time to get to them. But basically the cases marked with â are potentially bugs. For example in: https://github.com/cyberuni/typescript-module-resolutions-demo/blob/main/tests/node-es/test-result.4.9.4.md https://github.com/cyberuni/typescript-module-resolutions-demo/blob/main/tests/node16/test-result.4.9.4.md
That is one of the main problems. There are cases that we want to support both CJS and ESM, especially in corporate environment. Iâm sure you know how slow teams can move in those environments. As a library author, we canât just dump CJS and produce ESM only packages, while we add features to the packages. At the same time, we need to expose ESM code so that we can leverage the benefits of it in newer projects.
That is not the case, and is probably one of the reasons why there are some communication breakdown. Even without any âdependenciesâ, what we are saying is that TypeScript as it stands simply unable to produce Dual module packages that works for both ESM and CJS consumers.
The intended behavior of the Node16 module resolution mode is to follow Nodeâs module resolution logic, and in this case, TypeScript correctly does. This is a correct error.
react-use-websocket
âs type definition, and the runtime definition of its module, do not provide a function at the requested place.If you load
react-use-websocket
from a CommonJS module, you can see its public shape (running this code in a .js file from a non-type: "module"
repo):Its
.default
property is the function, as âexpectedâ.If you load
react-use-websocket
from an ES module, you can see its public shape as well:and the .default property isnât a function
The CJS<->ESM interop story, per Node and TypeScript (but not necessarily other bundlers, YMMV) is that a CJS module gets wrapped in the default export of the corresponding synthesized ESM entry point. Node and TypeScript agree here that the
esModule
flag doesnât do anything.So node and TypeScript 100% agree on this count: If you import this module from an ESM context, the object you get back (from the
*
import) has adefault
property with adefault
property. Thatâs the correct way to refer to that function.Really,
react-use-websocket
shouldnât be providing an export named"default"
in a CJS module, because itâs never (in a cromulent module configuration) going to be consumable from a default import form.