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
module.exports = style export.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:
import statements (probably unlikely due to Node.js team preferences)module.exports (most likely)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:
https://github.com/Microsoft/TypeScript/issues/19500
Maybe I've missed something, but can you use --esModuleInterop and use a default import for koa?
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.
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');
Also, esModuleInterop doesn't have any effect when targeting ES modules, right?
Ah, duh, right.
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"
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
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
@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'.
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
@ekilah with esModuleInterop you want
import Autosuggest from 'react-autosuggest'
@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 { ... }
Most helpful comment
besides
koa, there is some other use case:I have a lib
my_lib, and it depends onupper_lib:But I want
my_libsupport tree shaking, so: