Skip to content
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

TypeScript module "node16" does not work with CommonJS dependencies #49271

Closed
m-radzikowski opened this issue May 26, 2022 · 9 comments
Closed
Labels
External Relates to another program, environment, or user action which we cannot control.

Comments

@m-radzikowski
Copy link

m-radzikowski commented May 26, 2022

Bug Report

πŸ”Ž Search Terms

esm, cjs, axios

πŸ•— Version & Regression Information

TS 4.7.2

  • I was unable to test this on prior versions because the module "node16" is available only from version 4.7

⏯ Playground Link

Repository: https://github.com/m-radzikowski/ts-issue49271

πŸ’» Code

tsconfig.json:

{
  "compilerOptions": {
    "module": "node16",
    "target": "ES2021",
    "lib": [
      "ES2021"
    ],
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  }
}

index.ts:

import axios from 'axios';

await axios.get('https://google.com');

Run: tsc

πŸ™ Actual behavior

Error:

index.ts:3:13 - error TS2339: Property 'get' does not exist on type 'typeof import("/Users/mradzikowski/Documents/axios-esm/node_modules/axios/index")'.

πŸ™‚ Expected behavior

My understanding is the module "node16" introduced in TS 4.7 is meant to enable having the project use ESM while still being interoperable with CJS. Just like Node.js 16 is. Thus the ESM project could use CommonJS dependencies.

In this case, axios is a CommonJS dependency, without ESM dist. So I thought TS should handle it fine?

Contrary, the module "ES2022" added in TS 4.5 is a strict-ESM option, that requires the whole project to be strictly ESM, including dependencies. At least that was my understanding of the difference between those two settings.

#49160 was similar but there the conclusion was that the libraries were misconfigured. I'm not sure that's the case here - axios package.json has:

{
  "main": "index.js",
  "types": "index.d.ts",
  "typings": "./index.d.ts"
}

and no exports etc., which makes it a pure CJS package.

I may be (and probably I am) wrong somewhere here, so I would be glad for an explanation of that behavior.

@JacobLey
Copy link

You are on the right track, this is related to Typescript being overly protective about importing CJS in an ESM file.

The linked issue #49160 is the same idea.

This is what typescript sees: https://github.com/axios/axios/blob/master/index.d.ts#L332

Axios is declaring a default export in a CJS file. Typescript knows that ESM NodeJS will actually expose the module.exports as the "default import" rather than the actual default value, so it is warning you that you need to go access the correct value.

What typescript wants you do to is:

import axios from 'axios';

await axios.default.get('https://google.com');

But wait, this worked before? Why does a typescript bump mean the default import suddenly doesn't work?

Axios is declaring the default, but it is also assigning the module.exports to the axios instance as well! https://github.com/axios/axios/blob/master/lib/axios.js#L65

So it works as expected in Javascript. But they never told Typescript about it (missing an export = axios) so Typescript complains.

But you know it works that way, so you can do something like

import axios, { Axios } from 'axios';

(axios as unknown as Axios).get('https://google.com');

and use it as normal.

A little shameless self promotion, but the default-import package can help resolve this by properly resolving the default import + type in these weird cases

import axios from 'axios';
import { defaultImport } from 'default-import';

defaultImport(axios).get('https://google.com');

TLDR this is definitely a known confusion, and is working as intended in Typescript. The actual issue (if one exists at all) would be in Axios' type definitions. Hopefully one of these three work-arounds will get you back up and running

@unional
Copy link
Contributor

unional commented May 27, 2022

I think the problem is more than that.

The issue I found is that esModuleInterop is not working in this case.

I have this assertron library (written in TS) that exports assertron as the default export.

When in ESM, import a from 'assertron' fails. It pushes the default export to a.default.

@JacobLey
Copy link

I think that is behaving as expected...

The assertron library is written in CJS (TS allows it to be written as ESM, but the final result is outputted as CJS). There is an ESM version declared in the module field but NodeJS (and therefore TS) ignores that, looking for an exports or main.

So the same issue comes up that CJS does not have a concept of default export, but rather just includes a named export called default. So that is what you have to access instead of the "native" default.

I believe the esModuleInterop option only applies to CJS files, to support ESM-like features, which isn't necessary in native ESM. So you have to manually extract the default export to replicate what esModuleInterop was doing for you.

@m-radzikowski
Copy link
Author

@JacobLey thank you for the detailed explanation. I'm afraid this will make the full transition to ESM long, as many packages that work well with TS and module=commonjs will not get updated soon...

@Josh-Cena
Copy link
Contributor

The reason is because axios "lied" about what it exports in the declaration. TS doesn't know that module.exports can be used as AxiosStatic. To be fully compliant, they have to write something like export = AxiosStatic & { default: AxiosStatic }. The same issue happens when you try to use CJS + checkJs.

@RyanCavanaugh RyanCavanaugh added the External Relates to another program, environment, or user action which we cannot control. label Jun 1, 2022
@RyanCavanaugh
Copy link
Member

Unfortunately we don't have any good ways to force external packages to be correctly configured, and the overall design of node modules means that range of misconfigurations we're able to account for isn't very accommodating.

@ritschwumm
Copy link

just a shot in the dark: maybe typescript could help those external packages to configure themselves correctly somehow?

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'External' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@bohdyone
Copy link

bohdyone commented Oct 4, 2022

The reason is because axios "lied" about what it exports in the declaration. TS doesn't know that module.exports can be used as AxiosStatic. To be fully compliant, they have to write something like export = AxiosStatic & { default: AxiosStatic }. The same issue happens when you try to use CJS + checkJs.

I find it strange that TS knows that that there is a .default with the correct type on the export but can't use it with this syntax like it used to with esModuleInterop and "moduleResolution": "Node".

I agree with @m-radzikowski that this is a major friction point, and breaks the existing ecosystem. This should at least be handled by another optional interop mode. Surely can implemented similar to @JacobLey's library.

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
External Relates to another program, environment, or user action which we cannot control.
Projects
None yet
Development

No branches or pull requests

8 participants