microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.05k stars 12.37k forks source link

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

Closed otakustay closed 2 years ago

otakustay commented 2 years ago

Bug Report

🔎 Search Terms

class, not constructable, has no construct signatures

🕗 Version & Regression Information

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

weswigham commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

weswigham commented 2 years ago

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

otakustay commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 https://github.com/webpack-contrib/eslint-webpack-plugin/pull/140 on them with a fix, but they've had the issue since they first added generated types.

typescript-bot commented 2 years ago

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

SalamandraDevs commented 1 year ago

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.