microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.58k stars 12.43k forks source link

Synthesize namespace records on CommonJS imports if necessary #16093

Closed DanielRosenwasser closed 6 years ago

DanielRosenwasser commented 7 years ago

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
guybedford commented 6 years ago

Although synthetic defaults still seem to work side-by-side with namespace interpretations. It would have to be a form of synthetic that disallowed non-default named exports entirely.

weswigham commented 6 years ago

@guybedford for my own reference for such a future flag, I should note that import d from "cjs";, import {default as d} from "cjs";, and import * as o from "cjs"; const d = o.default; should all be valid (and identical) in such a scheme, so it's not just some simple syntactic ban on no default forms.

guybedford commented 6 years ago

Sure thanks @weswigham for noting it. It's good to prepare for this to dampen the landing as modules hit platform reality.