-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
This expression is not callable for ESM consuming CJS with default export #52086
Comments
Kind of related issues: #50466 But not directly related because TS transpiles CJS with type definition mismatch in the first place, |
I have the same problem and stumbled upon the following workaround from a question about the same exact problem on stackoverflow. import bar from 'foo';
export const foo = bar as unknown as typeof bar.default; It works, but it is far from convenient if you have to do this in several files for multiple packages over a large codebase. |
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. If you load
Its If you load
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 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 Really, |
The problem is, that is exactly what IMO it is just wrong (or put in another way, some bugs through the workflow), that TypeScript cannot consume code generated by TypeScript. |
It absolutely can consume it, just not with the syntax you're trying to use. The TypeScript project emitted a .d.ts file, which was consumed by another TypeScript project, with 100% fidelity with what the runtime allows for. Had TypeScript not issued an error here, you would have gotten a runtime error instead! It's hard to see how that is preferable.
The project is authored with ESM-style export syntax but targets CJS. So you have a CJS output with export names that align with the ESM export names, which is presumably what you want to happen. I'm open to hearing proposals as to what should happen instead, if this is "wrong". That CJS module then gets wrapped into an ESM module wrapper by Node, leading to the double- |
There is a lot more than just the double- The problem is with other cases. There are many situations that TypeScript typings say one way, but the runtime works differently. Again you can refer to the repo to see which one fails. The bottom line is, I think TypeScript needs to adjust on both how the typings are generated as well as how they are consumed. e.g. in a Node.js dual package repository, i.e. and when consuming the typings, in |
Sure, but this bug is about a situation where TypeScript is exactly mimicking the runtime.
Please log a specific bug about which line of emit or resolution is wrong.
Again, how? What? Be specific.
This is handled correctly, but the package linked in this repro is not a dual package. It doesn't have We've investigated several dozen "TypeScript is doing modules wrong" issues at this point and, at time of writing, all of them recently have been module misconfigurations rather than bugs in TypeScript. I'm going to be setting the bar higher on future bug reports so we can move on and do other work - if there's a claim that something in this space isn't working, it needs to be clearly demonstrated.
Correct; our position is that you should pick one runtime target (CJS or ESM) and write CJS or ESM code. Consumers can use either through well-trod interop or bundling processes. Producing a single artifact for both CJS and ESM is really only possible when all of your dependencies adhere to a set of coincidences that are not widely correctly adopted and we don't think this is a good model for publishing packages in general. |
Yes, I actually agree. This bug corresponding to the
Will do as I'm going through the cases in https://github.com/cyberuni/typescript-module-resolutions-demo
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.
That is not the case, and is probably one of the reasons why there are some communication breakdown. |
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. |
Thanks for the detailed reply (as always). From what you said. My understanding is this:
Why I say that is because while you mentioned
That's why we are stuck there. The repo does not demonstrate those cases yet. UPDATE: note that "the another tool" needs to consume and transform both JS and typings. IMO that would be best handled by TypeScript, or else "the another tool" will be tightly coupled to a specific version of TypeScript and will break whenever UPDATE 2: If what I expect is correct, this should be printed in bold in the TypeScript docs as this would trip over so many package authors. |
TypeScript has at best the same, and possibly worse, information than any other tool could have about whatever speculative transform ESM->CJS (just for the sake of argument) might exist. The typings as they exist today do not carry sufficient information on their own to inform how to correctly do this transform. Today in TS you can write declare module "foo" { export const a = 3 } import * as x from "foo"; and this all will just happily work. We have no idea whether Similarly you could write declare module "foo/bar.js" { export const a = 3 } and TypeScript has no idea if that means we're going to be directly loading the This is why we're so insistent on not messing with import paths (and to a lesser degree really regret providing a means to convert ESM to CJS syntax during downleveling without throwing up red flags everywhere) -- the typings that most projects have do not provide enough information to correctly guess the right output syntax, and even if they could, I think the lived experience of the Node16 transition shows that most package authors couldn't write the correct metadata even if it was mechanically possible to do so. The closest true source of "typings data", if it exists, is what's in Either one of two things has to be true:
Honestly most people here mistakenly think that the first one is true (i.e. you can just slap So again none of this is to say that we don't want to fix bona fide bugs in this space, just that the lack of some cross-targeting functionality is an intentional line in the sand we're drawing. We haven't rewritten esbuild/webpack/parser/terser/rollup/etc and are in fact removing product options which mimic those tools, and in fact are thinking about how we might exit the downleveling space as well. I think that's the right call for all of these spaces, both from our resource manage perspective, and for how TS should slot into the broader ecosystem. |
Um.... I can see your points. For you, you have to think about all the possible cases where user "adding" type information through My perspective is that writing only in TypeScript and using the TS file as the source of truth is probably the majority case. And that case, Yes, if you throw in additional typing files and/or dependencies with typings (AND differences between bundled typing files generated by It is a very difficult task but at the same time we try to figure out some possible solutions to make it works somehow. I'll continue to investigate about it and probably make some posts or videos about this topic, as I think the right first steps is to have those not working use cases drawn out. |
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
btw, I just realized one thing, to specify a different typings for CJS when using the
Unless you keep the
|
I'm very sorry, but I've followed through the thread and it looks similar to my issue but I'm still thinking TS behavior is incorrect. I got pretty straightforward file with sole The accompanied type reflects this default export with import emitter from '@streetstrider/emitter'
console.log(typeof emitter)
const e = emitter() If I try to import this in ESM mode with bare Node I will get the value of the
However, under TS I got:
Despite both types and Node intended behavior points to normal function in that position. If I bypass this typecheck error by workaround mentioned above, the code will work as expected (calling function and go on) without actual code changing. Thus it is a TS typecheck error. The underlying ESM code works normally in pure Node ESM and under ts-node. This issue got similar repro in softonic/axios-retry#228. |
Is it possible for someone to double-check this issue and maybe my results? |
@StreetStrider certainly you have a misconfiguration somewhere. Please log a bug under the "module resolution" template to provide the necessary info. |
@RyanCavanaugh thanks for the help. As it turns out, I had a misconception, thinking that |
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
⏯ Playground Link
Repro link: https://github.com/cyberuni/ts-esm-on-cjs-with-default-export
💻 Code
tsconfig.json:
🙁 Actual behavior
This expression is not callable
🙂 Expected behavior
work
The text was updated successfully, but these errors were encountered: