Skip to content

Synthesize namespace records on CommonJS imports if necessary #16093

Closed
@DanielRosenwasser

Description

@DanielRosenwasser

TL;DR

Flags like --allowSyntheticDefaultImports will just work without extra tools like Webpack, Babel, or SystemJS.

TypeScript will just work with the --ESModuleInterop flag without extra tools like Webpack, Babel, or SystemJS.

See the PR at #19675 for more details.

Background

TypeScript has successfully delivered ES modules for quite some time now. Unfortunately, the implementation of ES/CommonJS interop that Babel and TypeScript delivered differs in ways that make migration difficult between the two.

TypeScript treats a namespace import (i.e. import * as foo from "foo") as equivalent to const foo = require("foo"). Things are simple here, but they don't work out if the primary object being imported is a primitive or a value with call/construct signatures.
ECMAScript basically says a namespace record is a plain object.

Babel first requires in the module, and checks for a property named __esModule. If __esModule is set to true, then the behavior is the same as that of TypeScript, but otherwise, it synthesizes a namespace record where:

  1. All properties are plucked off of the require'd module and made available as named imports.
  2. The originally require'd module is made available as a default import.

This looks something like the following transform:

// Input:
import * as foo from "foo";

// Output:
var foo = __importStar(require("foo"));

function __importStar(mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) {
        for (var k in mod) {
            if (Object.prototype.hasOwnProperty.call(mod, key)) {
                result[k] = mod[k]
            }
        }
    }
    result.default = mod;
    return result;
}

As an optimization, the following are performed:

  1. If only named properties are ever used on an import, *the require'd object is used in place of creating a namespace record.

    Note that this is already TypeScript's behavior!

  2. If

    1. a default import is used at all in an import statement and
    2. if the __esModule flag is not set on the require'd value, then

    A fresh namespace record whose default export refers to the require'd value will be created in place of the require'd value.
    In other words:

    // This TypeScript code...
    import { default as d } from "foo";
    d.member;
    
    // Would become this CommonJS JavaScript code...
    var foo = require("foo");
    var foo_2 = foo && foo.__esModule ? foo : {default: foo};
    foo_2.default.member;

Note that there is no optimization for never using the default. This is probably because you'd need alias analysis and could never be sure that someone else would use the default.

Proposal

I believe that we'd serve both communities well if we decided to adopt the aforementioned emit.

Drawbacks

Performance & Size Impact

This certainly impacts the size and readability of TypeScript's output.
Furthermore, for large CommonJS modules, there could be a speed impact to keep in mind - notice that these synthesized namespace records are not cached at all.

Tools already do this for us

Those who've been using tools like Webpack 2 and SystemJS have been getting this functionality for a few months now.
For example, if you target ES modules, Webpack 2 will already perform this behavior.
Taking this strategy makes it an opt-in for users who care about the interop behavior.

I mainly bring this up because I want to immediately bring up the counterpoint.
While these tools are definitely central to many modern web stacks, they won't cover

  1. Vanilla Node.JS users
  2. React Native Packager users
  3. Browserify users (duh)
  4. Users whose test code doesn't run through these tools

Metadata

Metadata

Assignees

Labels

CommittedThe team has roadmapped this issueDomain: ES ModulesThe issue relates to import/export style module behaviorFixedA PR has been merged for this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions