Skip to content

Import assignment not allowed when module format is ES modules #22321

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

Closed
justinfagnani opened this issue Mar 4, 2018 · 12 comments
Closed

Import assignment not allowed when module format is ES modules #22321

justinfagnani opened this issue Mar 4, 2018 · 12 comments
Labels
Domain: ES Modules The issue relates to import/export style module behavior Question An issue which isn't directly actionable in code

Comments

@justinfagnani
Copy link

justinfagnani commented Mar 4, 2018

TypeScript Version: 2.7.2

Search Terms: import assignment modules import = require types

Code

When targeting CommonJS, I can pull in values and types from a CommonJS module like so:

import Koa = require('koa');

Which generates a normal require statement:

const Koa = require('koa');

Actual behavior:

When targeting MS modules, import assignment triggers the warning:

[ts] Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.

The suggestions aren't available though, because

  1. This CJS module isn't ES module compatible, it uses an module.exports = style export.
  2. An ES import statement will emit and import statement, and I need it to emit require() to use with @std/esm (more on this latter).

The only way to generate a require() is to use one without import assignment, but then we have to jump through major hoops to import the types:

// Import just the type under a different name so there's not a clash in the value namespace
// Make sure you import only types so this isn't emitted!
import _Koa from 'koa';

// Alias just the type back to the class name
type Koa = _Koa;

// Re-declare the static interface because there's no way I know of to extract the
// static interface of a class in this situation.
interface KoaConstructor {
  new(): Koa;
}

// Normal require(), and cast to the static type
const Koa = require('koa') as KoaConstructor;

This is obviously pretty cumbersome.

Expected behavior:

I think supporting import assignment when emitting ES modules is the easiest solution. Just continue to emit:

const Koa = require('koa');

Obviously, we don't know how CJS/ESM interop is going to behave quite yet. The possibilities range from:

  1. Not supporting CJS in import statements (probably unlikely due to Node.js team preferences)
  2. CJS modules only having a default export with the value of module.exports (most likely)
  3. Somehow supporting named exports (seems difficult to impossible).

Regardless of the CJS interop support, it will at least possible to still use require() (maybe still a global, maybe via import.meta.require).

But even with interop, using require() is necessary for type-checking because existing typings describe what's returned by require(), not what would be returned from the CJS interop proposals. That is, existing typings do not describe an ES module with a default export. TypeScript may also need a way to transform typings under various interop schemes, but that's a separate issue.

So, currently at least, we need to emit require() instead import and also bring in the types. Import assignment does exactly this.

Another option could be to offer an easier way to import just the type of a module and cast the require() result, but that seems to only more verbosely describe what import assignment already does. Essentially it would be allowing this:

import * as _Koa from 'koa';
const Koa = require('koa') as _Koa;

Without the error that's generated from the import:

[ts] A namespace-style import cannot be called or constructed, and will cause a failure at runtime.

Playground Link: BTW, there's no option in the playground to set the "module" compiler option.

Related Issues:
#19500

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Mar 4, 2018

Maybe I've missed something, but can you use --esModuleInterop and use a default import for koa?

@justinfagnani
Copy link
Author

Maybe I'm missing something: I can't use an import at all because Koa isn't an ES6 module. I need to emit a require for it.

@justinfagnani
Copy link
Author

Maybe it's confusing because I didn't include an import in the examples:

I want this:

import * as foo from './foo.js';
import Koa = require('koa');

to output:

import * as foo from './foo.js';
const Koa = require('koa');

@justinfagnani
Copy link
Author

Also, esModuleInterop doesn't have any effect when targeting ES modules, right?

@DanielRosenwasser
Copy link
Member

Ah, duh, right.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Mar 4, 2018

But in that case, your loader (@std/esm) will do the heavy lifting for you, so I think you should be able to use allowSyntheticDefaultImports with "module": "esnext"

@xialvjun
Copy link

besides koa, there is some other use case:

I have a lib my_lib, and it depends on upper_lib:

// upper_lib
class Upper {}
export = Upper

// my_lib
import Upper = require('upper_lib'); // I have to use import assign because upper_lib is 'export = Upper'
export class MyClass extends Upper {}

But I want my_lib support tree shaking, so:

// tsconfig.json
"module": "esnext"

// my_lib
import Upper = require('upper_lib');
// report error: Import assignment cannot be used when targeting ECMAScript modules.
// tsconfig.json
"module": "esnext"

// my_lib
import * as Upper from 'upper_lib';
// report error: Module 'upper_lib' resolves to a non-module entity and cannot be imported using this construct.
// tsconfig.json
"module": "commonjs"

// my_lib
import Upper = require('upper_lib');
// OK, but not what I want - no tree shaking.
// tsconfig.json
"module": "commonjs",
"esModuleInterop": true,

// my_lib
import Upper from 'upper_lib';
// OK, but not what I want - no tree shaking.
// tsconfig.json
"module": "esnext",
"esModuleInterop": true,

// my_lib
import Upper from 'upper_lib';
// report error: Module 'upper_lib' has no default export.
// tsconfig.json
"module": "esnext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,

// my_lib
import Upper from 'upper_lib';
// OK, but other project depends 'my_lib' need to set then same tsconfig

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@justinfagnani
Copy link
Author

@DanielRosenwasser I think this issue is still relevant, especially as module interop is defined on the node side of things. Basically all the Definitely Typed typings assume that you can have named exports from CJS modules, which 1) doesn't work if you have to require() CJS and 2) doesn't work if CJS doesn't have named exports, but default exports the exports object.

So I think import assignment should work in module output, so you can import Koa = require('koa') with current types properly, and it seems likely that there will need to be some way to interpret types differently based on how the loader works, ie if node doesn't allow named exports from CJS, then interpret import koa from 'koa' as essentially import * as koa from 'koa'.

@ekilah
Copy link

ekilah commented Dec 20, 2018

as someone who doesn't really know much about any of this CJS vs ES Modules nonsense, I've arrived here after enabling esModuleInterop when trying to figure out how to import an npm package & it's types I don't control.

is there any update, 6 months later, on the proper way to import a package that uses statements like export = PackageName; export namespace PackageName{...} in it's typings file? I would like to use the typings AND the functionality from one import (since two imports clash with the same name), but as the OP @justinfagnani says, it seems like that's not possible with esModuleInterop enabled?

again, I don't really have a clear and full understanding of all the complexities, I'm just trying to use a library.

for context/ an example:

import * as Autosuggest from 'react-autosuggest' // what I had before `esModuleInterop`, which was great

import Autosuggest = require('react-autosuggest') // complains that this doesn't work with EMCAScript modules as a target

const Autosuggest = require('react-autosuggest') // works, but types are not imported, so lots of red lines appear

// @ts-ignore
import Autosuggest = require('react-autosuggest') // not too surprisingly, fails at runtime

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Dec 20, 2018

@ekilah with esModuleInterop you want

import Autosuggest from 'react-autosuggest'

@ekilah
Copy link

ekilah commented Dec 20, 2018

@DanielRosenwasser sorry, I left that case out. Still doesn't work:

import Autosuggest from 'react-autosuggest' // TS1129: Module '".../node_modules/@types/react-autosuggest/index"' has no default export

import {Autosuggest} from 'react-autosuggest' // TS2305: Module '".../node_modules/@types/react-autosuggest/index"' has no exported member 'Autosuggest'.

and here's the top of the types file for reference:

import * as React from 'react';

declare class Autosuggest<T = any> extends React.Component<Autosuggest.AutosuggestProps<T>> {}

export = Autosuggest;

declare namespace Autosuggest { ... }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: ES Modules The issue relates to import/export style module behavior Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants