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

importing a class from a CJS package inside ESM package complains "not constructable" #47332

Closed
otakustay opened this issue Jan 6, 2022 · 11 comments
Assignees
Labels
External Relates to another program, environment, or user action which we cannot control.

Comments

@otakustay
Copy link

otakustay commented Jan 6, 2022

Bug Report

πŸ”Ž Search Terms

class, not constructable, has no construct signatures

πŸ•— Version & Regression Information

  • I was unable to test this on prior versions because it's related to nodenext module

⏯ Playground Link

Playground link with relevant code

Change Module configuration to NodeNext to reproduce errors.

πŸ’» Code

import ESLintPlugin from 'eslint-webpack-plugin';

const plugin = new ESLintPlugin();

πŸ™ Actual behavior

This simple code inside a "type": "module" package with tsconfig.json declaraing "module": "nodenext", "moduleResolution": "nodenext" can result in type errors:

import ESLintPlugin
This expression is not constructable.
  Type 'typeof import("/Users/otakustay/Downloads/s/node_modules/eslint-webpack-plugin/declarations/index")' has no construct signatures.ts(2351)

πŸ™‚ Expected behavior

Since TypeScript already resolves type definition to correct .d.ts, and this type definition seems correct:

export default ESLintWebpackPlugin;
export type Compiler = import('webpack').Compiler;
export type Options = import('./options').Options;
declare class ESLintWebpackPlugin {
  // ...
}

This code should compile as expected.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jan 11, 2022
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.6.0 milestone Jan 11, 2022
@weswigham
Copy link
Member

You wanna say new ESLintPlugin.default(). This is because your package is esm, while eslint-webpack-plugin (as far as we know) is CJS. The thing about node-native esm is that it loads the whole cjs module as the default (unlike in interop land where that's disabled with a runtime flag).

I'm gunna look into seeing if we can issue a better error here that can help indicate what the correct solution is.

@otakustay
Copy link
Author

Despite the type error, I'm able to import it's default properly in Node, I'm wondering why type check is not aligned with runtime behavior:

Welcome to Node.js v16.13.0.
Type ".help" for more information.
> const {default: Plugin} = await import('eslint-webpack-plugin')
undefined
> new Plugin()
ESLintWebpackPlugin {
  key: 'ESLintWebpackPlugin',
  options: {
    extensions: 'js',
    emitError: true,
    emitWarning: true,
    failOnError: true
  },
  run: [Function: bound run] AsyncFunction
}

How can we fix the type declaration to ensure it works in both ESM and CJS environment?

@weswigham
Copy link
Member

Does eslint-webpack-plugin expose an esm entry point? If so, exposing types for that entry point in its types would be correct.

@weswigham
Copy link
Member

(Alternatively, the types may just be wrong in saying that it's a default export, and a export = may be correct)

@otakustay
Copy link
Author

Sadly TypeScript is unable to export types along with a export = statement:

error TS2309: An export assignment cannot be used in a module with other exported elements.

59 export = FooBar;
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Maybe this (export types along with export =) could be a feature request?

@weswigham
Copy link
Member

eslint-webpack-plugin's actual runtime entrypoint is

"use strict";

const plugin = require('./');

module.exports = plugin.default;

It shouldn't declare a default at all - just an export=.

@weswigham
Copy link
Member

Sadly TypeScript is unable to export types along with a export = statement:

You just move the other types-to-be-exported into the namespace associated with the export='d thing. Eg,

class Plugin {
 // ...
}
export = Plugin;
namespace Plugin {
  export interface Whatever {} // like this
}

weswigham added a commit to weswigham/eslint-webpack-plugin that referenced this issue Feb 10, 2022
Per [this issue](microsoft/TypeScript#47332) over on the TS repo, the current shape described by the types does not accurately describe the runtime shape of the entrypoint.
weswigham added a commit to weswigham/eslint-webpack-plugin that referenced this issue Feb 10, 2022
For other entrypoints, an export map would probably be appropriate. The current structure causes issues like microsoft/TypeScript#47332 where TS users can't actually accurately check the package.
@weswigham weswigham added External Relates to another program, environment, or user action which we cannot control. and removed Needs Investigation This issue needs a team member to investigate its status. labels Feb 10, 2022
@weswigham
Copy link
Member

The package in question actually already contains a definition for their cjs entrypoint, but their package.json doesn't point at it. I put up webpack-contrib/eslint-webpack-plugin#140 on them with a fix, but they've had the issue since they first added generated types.

@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.

@SalamandraDevs
Copy link

Sadly TypeScript is unable to export types along with a export = statement:

You just move the other types-to-be-exported into the namespace associated with the export='d thing. Eg,

class Plugin {
 // ...
}
export = Plugin;
namespace Plugin {
  export interface Whatever {} // like this
}

Hi, thanks to this response I was able to fix a problem with an old CJS module that I traied to use in a ESM Typscript project. I wondering where is the documentation about class exports with CJS to ESM compatibility.

@damianobarbati
Copy link

I'm having the same problem: since I moved to NodeNext to use the native node flag --strip-types, this not constructable started popping out everywhere!

{
  "compilerOptions": {
    "moduleResolution": "NodeNext",
    "module": "NodeNext",
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext", "WebWorker"],
    "allowJs": true,
    "allowImportingTsExtensions": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "noImplicitAny": false,
    "noUnusedLocals": false,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "strictPropertyInitialization": false,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": "."
  }
}

Is there any workaround?

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

6 participants