Skip to content

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

Closed
@justinfagnani

Description

@justinfagnani

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Domain: ES ModulesThe issue relates to import/export style module behaviorQuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions