Skip to content

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

Description

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.

Activity

  1. RyanCavanaugh commented on Jun 30, 2022

    @RyanCavanaugh
    Member

    Previously discussed at #47338 (comment) - this not working is intentional since it's a forward-compat hazard.

    The proposed solution at the time was to add import mode assertions, but people didn't like this (see comments #48644) and we couldn't agree on a different syntax that was acceptable, so moved import mode assertions to be nightly-only. There's issue #49055 tracking next steps on this issue.

  2. brillout commented on Nov 13, 2022

    @brillout
  3. slorber commented on Nov 18, 2022

    @slorber

    Not sure to understand why it doesn't, but for me just adding // @ts-expect-error is a good enough workaround to make my program typesafe without complicating things too much.

    // @ts-expect-error
    import type { ContainerDirective } from "mdast-util-directive";
  4. slorber commented on Apr 21, 2023

    @slorber

    In my case (building the Docusaurus framework packages), using // @ts-expect-error is not a good solution when those types are re-exported.

    The emitted .d.ts files contain the imports but // @ts-expect-error is not in the .d.ts output of the lib.

    This makes the framework users have compilation errors with skipLibCheck: false, such as:

    ../packages/docusaurus-mdx-loader/lib/loader.d.ts:9:32 - error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("unified")' call instead.
    
    9 import type { Pluggable } from 'unified';

    As far as I understand, Import attributes moved to stage 3 again with a new syntax (#53656)

    See also #53426 (comment)

    Do you think the following will fix the problem?

    import type { Plugin } from 'unified' with { "resolution-mode": "import" };

    Can we expect it to be available soon Ryan Cavanaugh (@RyanCavanaugh) ?
    Looks like you'd rather not rush things according to this comment: #53656 (comment)

    Is there any alternative workaround to the above solution in the meantime?


    I tried weird workarounds like this one

    // @ts-expect-error: :s
    async function getUnifiedPluginHack() {
      const {unified} = await import('unified');
      type Attachers = ReturnType<typeof unified>['attachers'];
      type Attacher = Attachers[number];
      type PluginP = Attacher[0];
      return null as any as PluginP;
    }
    type Plugin = Awaited<ReturnType<typeof getUnifiedPluginHack>>;

    And of course it didn't work 😅 TS error + the type is generic

    src/index.ts:15:16 - error TS2841: The type of this expression cannot be named without a 'resolution-mode' assertion, which is an unstable feature. Use nightly TypeScript to silence this error. Try updating with 'npm install -D typescript@next'.

    15 async function getUnifiedPluginHack() {

  5. voxpelli commented on Jun 3, 2023

    @voxpelli

    Duplicate of #52529 which has a fix in #53426 (I know this one was created first, but the other one has more activity)

  6. slorber commented on Oct 4, 2023

    @slorber

    Isn't TS 5.3 beta fixing this issue Andrew Branch (@andrewbranch) ?

    CleanShot 2023-10-04 at 14 58 42@2x
  7. andrewbranch commented on Oct 4, 2023

    @andrewbranch
    Member

    Yep!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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