Skip to content

Allow ES Module Type-Only Imports from CJS ModulesΒ #49721

Closed
@amiller-gh

Description

@amiller-gh

Suggestion

πŸ” Search Terms

es esm module cjs commonjs type import moduleResolution node16 nodenext

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Allow CommonJS modules to import types from ES Modules without an async import() if import types is specified.

With moduleResolution: "node16" enabled, we now get this very helpful error message when importing an ES Module from a CommonJS module (thank you!):

// Error: Module 'my-es-module' cannot be imported using this construct. The specifier only resolves to an ES module, which cannot be imported synchronously. Use dynamic import instead.
import MyModule from 'my-es-module';

However, it is often the case that module will export a types API as well as a runtime API. Sometimes, we only want to import the types from a package. Typescript has the very helpful import type directive to acommodate this use case. However, if we want to import the types from an ES Module, we still get the following error:

// Error: Module 'my-es-module' cannot be imported using this construct. The specifier only resolves to an ES module, which cannot be imported synchronously. Use dynamic import instead.
import type { MyTypeDefinition } from 'my-es-module';

Because import types are stripped from runtime code, there is no need to throw this error.

πŸ“ƒ Motivating Example

Because this error is thrown, it forces us to add an unnecessary async import somewhere in our file, causing non-ergonomic development at best, and unnecessary runtime complexity at worst.

Consider:

// ES Module "my-es-module"
export type MyTemplateString = `${string}-custom-string`;
function myFunction() {
  const tmp = await import('my-ex-module');
  const custom: tmp.MyTemplateString = 'contrived-custom-string';
  return custom;
}

It also (very unfortunately) makes it simply impossible to create and re-export derivative types from a module like this:

import type { MyTemplateString } from 'my-es-module';

export interface MyInterfaceDefinition {
  type: MyTemplateString | null;
  value: string;
}

πŸ’» Use Cases

See above motivating example. I'm currently using // @ts-expect-error to get past this, but would be great not to! This error should obviously still throw if importsNotUsedAsValues is set to preserve.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions