Skip to content

require(esm) 'module.exports' interop export name supportΒ #61645

Description

@sapphi-red

πŸ” Search Terms

require(esm), module.exports export, node 22.12

βœ… Viability Checklist

⭐ Suggestion

Node 20.19.0, 22.12.0, 23.0.0 added requiring ESM files (known as require(esm)). This feature includes module.exports interop export name feature that allows users to customize what the module.exports value would be when a module is required.
(nodejs/node#53848)

Currently TypeScript does not use the module.exports export type when requiring an ESM file.
I'd like TypeScript to use the module.exports export type for the types when that ESM file is required, aligning the type with the actual runtime object.

πŸ“ƒ Motivating Example

I made a reproduction: https://github.com/sapphi-red-repros/typescript-require-esm-module-exports-export
You can see how the types and the actual runtime values are different by the following steps

  1. Run pnpm i
  2. (Run pnpm build) (the built file is already included in the repo)
  3. Run node index.cjs
  4. See types in index.cts

The concrete differences are:

  • testPkg['module.exports' is undefined at runtime, but the type is { foo: string; default: { bar: string; }; bar: string; }
  • testPkg contains bar: string at runtime, but the type does not include that

nodejs/TSC#1622 (comment) describes the motivation of this module.exports export interop feature.

πŸ’» Use Cases

  1. What do you want to use this for?
    To migrate a package that had CJS files in past, to ESM-only while keeping the interface as-is without generating a separate type definition file.

  2. What shortcomings exist with current approaches?
    It requires two type definition types, one for require and one for import, so two builds are needed. The package.json would also be convoluted. main field cannot be used as the type definition file is different for require and import. The exports field would need to be like:

{
  "exports": {
    ".": {
      "types": { "require": "./index.d.cts" },
      "default": "./index.js"
    }
  }
}
  1. What workarounds are you using in the meantime?
    I haven't considered it yet. Probably I'll use the exports field workaround described above.

Activity

  1. andrewbranch commented on May 23, 2025

    @andrewbranch
    Member

    This has some tricky implications when the require comes from transpiled code. In a .cts file, you can write all of these:

    import b from "./foo.mjs";
    import * as c from "./foo.mjs";
    import { d } from "./foo.mjs";

    Each transpiles to a require("./foo.mjs") plus some other stuff. For these to have valid semantics, require("./foo.mjs") would need to return a module namespace object (or at least a non-callable object). I think we should just issue an error on import declarations that transpile to require that resolve to ES modules with a "module.exports" export.

  2. sapphi-red commented on May 24, 2025

    @sapphi-red
    Author

    I think we should just issue an error on import declarations that transpile to require that resolve to ES modules with a "module.exports" export.

    Does this mean that users would have to use import foo = require('foo.mjs') if foo.mjs has module.exports export?

    If this behavior is adopted, how should package authors generate the JS and DTS files if they want to keep the same behavior (keeping import foo from 'foo' work) while migrating to ESM from CJS? Should they add a module.exports export in the JS file but remove it from the generated DTS file?

  3. andrewbranch commented on Jun 12, 2025

    @andrewbranch
    Member

    Fixed by #61805

    Does this mean that users would have to use import foo = require('foo.mjs') if foo.mjs has module.exports export?

    Upon more careful analysis, no. In general, transpiled imports just work. There are some complexities around import * and whether you have esModuleInterop enabled, but those rules fall out of existing rules for what imports can resolve to what exports.

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

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions