-
Notifications
You must be signed in to change notification settings - Fork 5.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
npm default export is incorrectly typed for clsx #19096
Comments
It looks like the file it's trying to resolve doesn't exist in my file system. It's trying to resolve If I explicitly set My main point of confusion is why deno itself does not raise any red flags, but it's throwing compiler errors here. |
I also just ran into this bug twice with two other packages (postcss-nesting and windicss). So this bug appears to be more general and less specific to clsx. The following should type check: import postcssNesting from "npm:[email protected]";
postcssNesting(); but
Likewise the following should type check: import Processor from "npm:[email protected]";
const processor = new Processor(); but
Deno is no longer using proper TypeScript but has started using a fork to support npm specifiers, see #16332. |
Yeah I meant to use clsx as an example but I guess the title got renamed.
That's good to know. I assume that's causing the discrepancy because VSCode will not be able to use that fork, is that correct? I guess in the end most people are using URL imports nowadays but certain packages (Vite in particular comes to mind) work with the |
No that's not correct. You can just install the Deno extension for VSCode, which uses the language server that's part of the
Deno 1.28 has stabilized npm compatibility in the sense that you no longer need to use |
This has to do with Deno's Node module resolution implementation for type checking and is not related to the fork (the fork is for multiple
https://arethetypeswrong.github.io/?p=clsx%401.2.1 |
Ah, thanks for clarifying that. I am curious: is the node module resolution implemented in Rust? Could you point me to where it's implemented? |
It's called here from TypeScript: deno/cli/tsc/99_main_compiler.js Lines 600 to 603 in dd508c9
Then rust code starts here (see call to Line 515 in dd508c9
|
Ah, thanks! Are you sure that this problem only occurs with packages that have incorrect types? If so what's wrong with the |
@not-my-profile it's a cjs dts file that has a default export. |
I'm finding that trying to use default exports from NPM packages often causes type errors, but simply copying the type declaration file to a local file and referencing it with Here's an example: import analyzerPlugin from "npm:[email protected]";
analyzerPlugin(); The type error is: The code runs fine, however. Are The Types Wrong reports: "? Missing The types say: declare const analyzer: (options?: AnalyzerOptions) => Plugin;
export default analyzer; And as mentioned, when I copy the contents of |
This is actually not a bug. The same thing happens in typescript when using NodeNext module resolution from ESM (which Deno is): In the case of clsx, the types of newer versions are correct now, so upgrading to a new version should fix the issue. With [email protected], it has incorrect types and its default export is described as not callable with NodeNext module resolution from ESM:
The reason it works is because when you copy to a local file, that file is considered ESM instead of CJS (because it's not in a cjs npm package anymore) and so TypeScript treats the default export as an ESM default export instead of a CJS with a I know, this whole ESM importing CJS situation really sucks, but the types are wrong for those packages and they don't properly describe themselves. Luckily https://arethetypeswrong.github.io is helping and more and more packages are having correct types over time. |
Thanks for the quick reply. I hear your argument that Deno's decision not to support these NPM packages is ideologically correct. However, I think the way you are imagining this will play out is just not realistic.
I am actually a bit skeptical that the suggestion to " Reading what the TypeScript handbook has to say about writing types for CommonJS modules:
If we look in module.exports = plugin; and in declare const analyzer: (options?: AnalyzerOptions) => Plugin;
export default analyzer; (It's true that the JS also executes So, while I may have missed something in citing the TypeScript handbook, that's that I would expect a package author to say: That they followed the best practices for writing types for CommonJS modules. What can I say to that? And even if I can be convinced those types are just plain wrong, we still have to convince the entire world. Maybe there could be some kind of flag or simpler workaround than what I'm doing. It doesn't seem like Deno needs to be confused about what is happening with this package. |
I see that the types I cited require |
To add a little more information, not to take up anyone's time, but perhaps for the benefit of someone coming across this thread in the future... There is good info in the DefinitelyTyped README, and DefinitelyTyped uses arethetypeswrong (so packages whose types come from DefinitelyTyped will not have arethetypeswrong errors). The README does say:
In the FAQ it says (which seems slightly contradictory):
In the case of But, it does say earlier in the document to use |
See this setup and output:
If I change the call site to be incorrect like so, there's an error when type checking and at runtime: So the types need to accurately describe what they are at runtime. If they say
Yes, that's correct. This is the way that types should describe themselves to be accurate. A big confusion comes from the fact that TypeScript supports |
I see, so the const plugin = (opts = {}) => {
// ... snip ...
};
Object.assign(plugin, { plugin, analyze, formatted, reporter });
plugin.default = plugin; // <--- this line
module.exports = plugin; When I use declare const analyzer: (options?: AnalyzerOptions) => Plugin;
export default analyzer; Note that this package is written in JS, not TS, and the author attempted to satisfy a range of different types of consumers. (There is even a What I don't understand is, when I don't specify import analyzePlugin from "npm:[email protected]";
analyzePlugin(); ...what is Deno doing with the I know you said it had something to do with erasing the CommonJS status of a module, but I still don't understand why setting a module's types to its own types fixes the type error: // @deno-types="https://unpkg.com/[email protected]/index.d.ts"
import analyzePlugin from "npm:[email protected]";
analyzePlugin(); // works I think if I understood why this JS code and these types work at runtime but don't type-check (without the extra directive), that would help. Because it actually seems like the developer covered their bases. For now, I'm probably going to just keep having a copy of this module's types checked into my repository, because they are short, and it enables me to work around some other issues at the same time. |
Specifically, when Deno has the types for an NPM package, I would expect it to use those types for type-checking and not the JS. I remember that modules in Deno have a "code slot" and a "type slot," as described here. Deno "will look at the slots for the dependency, offering [the TypeScript compiler] the type slot if it is filled before offering it the code slot." |
When you use
It's using the types for type checking—Deno is not looking at the index.js file to figure out the types. The index.d.ts file in rollup-plugin-analyzer is considered a CJS module because the package.json does not contain
The |
As I've said, there is a I decided to see what happens with importing this module in a TypeScript project, without Deno. I based my tsconfig.json on yours: {
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"lib": ["ESNext", "DOM"]
}
} I installed the package and put this in import plugin from "rollup-plugin-analyzer"
console.log(plugin()); This works perfectly. TypeScript emits the right code to do the necessary CJS interop. But renaming the file To make import plugin from "rollup-plugin-analyzer"
console.log(plugin.default()); I'm learning this is by design (of Node's and TypeScript's latest ESM/CJS interop story), as ugly as it is. And this code works at runtime, too! Thanks to that line where the I say newfangled because... something major changed about what ESM, CJS, and interop even mean in TypeScript, once Node embarked on trying to support ESM modules. I think I'm putting the historical picture together, now.
module.exports = function foo() { return 123; } Meanwhile, you have "ES modules" like this output from the TypeScript compiler: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = foo;
function foo() { return 123; } It sets The whole point of interop, at the time of If an Then Node apparently decided that it would do its own thing with ESM/CommonJS interop, when it shipped native ESM. It didn't buy into the TypeScript/Babel mapping from ESM to CJS where you have In a bizarre turn of events, ESM-transpiled-to-CJS—the bread and butter of tools like TypeScript—which used to be on the "ESM" side of ESM/CJS interop side, with the CJS side consisting of non-transpiled, human-written CJS modules, is now considered to be on the "CJS" side of interop, with native ESM on the other side. The interop with idiomatic CJS is gone in the new module modes, after being the de facto standard, and part of TypeScript's emit, for years. Pushing the narrative that package authors just had the "incorrect" types the whole time is misguided at best. Based on my research, the Node situation is widely considered to be a mess, and developer sentiment is pretty negative about the behavior of For example, consider these Hacker News comments:
Or these comments from the maintainer of a bundling tool, when asked to add support for generating
Developers who encounter issues importing CJS from ESM with the new TypeScript behavior get a range of responses from commenters, with some echoing your take that all the responsibility belongs to package authors writing wrong types, but there is also acknowledgment that the old standard of interop has been lost, the new behavior is quite different, and the situation is not ideal:
Zooming out in order to not miss the forest for the trees... Deno wants to provide a good experience of using NPM packages. Node did some crazy stuff that kind of breaks the ecosystem, and TypeScript is trying to play along as best it can, because what is it supposed to do? (Though I think they probably had some better options than what they settled on, and maybe they will still ship some kind of improved interop. It's possible people don't run into, and report, this issue very often because not many people use Anyway, it sounds like Deno is giving its developers the Deno isn't Node, and it actually has control over how interop works. It's not in the position the TypeScript team is in, or the position that DefinitelyTyped is in. It doesn't have to just rationalize and live with other people's decisions. Deno can choose TypeScript flags, it can have shims, it can do whatever it wants. At the very least, let's not rewrite history or risk gaslighting people. TypeScript wrote new rules for Here's a good TypeScript module theory page, to leave it here as a reference for people. There's a particularly interesting part a reader might skip to, about how TypeScript mis-predicted (not that they could have known!) how ESM would interoperate with CJS when it finally landed, resulting in today's misalignment:
I'll close this very long comment (thanks for reading) by saying, I think it's important to consider not just one narrative and notion of "technical correctness" here, but multiple viewpoints, and also what developers want and what's good for developer experience. Also, if you want a bunch of developers to go do a bunch of work updating packages, you have to be able to validate their lived experience. Going around projecting a reality where people are just wrong and therefore need to fix their packages might influence a few people, but I think a more grounded approach is more effective. And as I mentioned, Deno is in a position where it could take any of a variety of stances on the issue. I welcome technical and historical corrections, and engagement with some of the themes here. |
We're not going to invent yet another way of doing Node resolution. That would make the world a worse place.
I'm not sure what you mean by rewriting history. Node resolution was implemented without
That is incorrect/outdated information and it's not properly describing the CJS files as having a |
Describe the bug
npm:*
default exports are incorrectly typed. Packages that are exported asdefault
are assumed to have adefault
property. The plugin will claim a type error when there are no actual runtime errors. Furthermore, trying to appease the plugin error by using thedefault
property will actually result in runtime errors.To Reproduce
script.ts
Try running
deno run script.ts
, notice there are no errors at runtimeTry appeasing the compiler:
Expected behavior
Expected plugin parsing behavior to match runtime behavior.
Screenshots
Versions
vscode: 1.77.3 deno: 1.33.0 extension: 3.17.0
The text was updated successfully, but these errors were encountered: