Skip to content

Commit

Permalink
Re-do the esmoduleinterop docs
Browse files Browse the repository at this point in the history
  • Loading branch information
orta committed Aug 7, 2020
1 parent 67b0ef9 commit 2e3a872
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,42 @@ instead of:
import * as React from "react";
```

When the module **does not** specify a default export.
When the module **does not** explicitly specify a default export.

For example, without `allowSyntheticDefaultImports` as true:

```ts twoslash
// @checkJs
// @allowJs
// @filename: utilFunctions.js
// @noImplicitAny: false
const getStringLength = (str) => str.length;

module.exports = {
getStringLength,
};

// @filename: index.ts
import utils from "./utilFunctions";

const count = utils.getStringLength("Check JS");
```

This code raises an error because there isn't a `default` object which you can import. Even though it feels like it should.
For convenience, transpilers like Babel will automatically create a default if one isn't created. Making the module look a bit more like:

```js
// @filename: utilFunctions.js
const getStringLength = (str) => str.length;
const allFunctions = {
getStringLength,
};

module.exports = allFunctions;
module.exports.default = allFunctions;
```

Turning on this compiler flag will also enable [`allowSyntheticDefaultImports`](#allowSyntheticDefaultImports).

This does not affect the JavaScript emitted by TypeScript, it only for the type checking.
This option brings the behavior of TypeScript in-line with Babel, where extra code is emitted to make using a default export of a module more ergonomic.
81 changes: 55 additions & 26 deletions packages/tsconfig-reference/copy/en/options/esModuleInterop.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,70 @@ display: "ES Module Interop"
oneline: "Emit additional JS to ease support for importing commonjs modules"
---

Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports.
By default (with `esModuleInterop` false or not set) TypeScript treats CommonJS/AMD/UMD modules similar to ES6 modules. In doing this, there are two parts in particular which turned out to be flawed assumptions:

TypeScript adheres to the EcmaScript standard for modules, which means that a file with exports would have to specifically
include a `default` export in order to support syntax like `import React from "react"`.
This export pattern is rare in modules for CommonJS. For example, without `esModuleInterop` as true:
- a namespace import like `import * as moment from "moment"` acts the same as `const moment = require("moment")`

- a default import like `import moment as "moment"` acts the same as `const moment = require("moment").default`

This mis-match causes these two issues:

- the ES6 modules spec states that a namespace import (`import * as x`) can only be an object, by having TypeScript
treating it the same as `= require("x")` then TypeScript allowed for the import to be treated as a function and be callable. This breaks the spec's recommendations.

- while accurate to the ES6 modules spec, most libraries with CommonJS/AMD/UMD modules didn't conform as strictly as TypeScript's implementation.

Turning on `esModuleInterop` will fix both of these problems in the code transpiled by TypeScript. The first changes the behavior in the compiler,the second is fixed by two new helper functions which provide a shim to ensure compatibility in the emitted JavaScript:

```ts
import * as fs from "fs";
import _ from "lodash";

fs.readFileSync("file.txt", "utf8");
_.chunk(["a", "b", "c", "d"], 2);
```

With `esModuleInterop` disabled:

```ts twoslash
// @checkJs
// @allowJs
// @allowSyntheticDefaultImports
// @filename: utilFunctions.js
// @noImplicitAny: false
const getStringLength = str => str.length;
// @noErrors
// @showEmit
// @esModuleInterop: false
// @module: commonjs
import * as fs from "fs";
import _ from "lodash";

fs.readFileSync("file.txt", "utf8");
_.chunk(["a", "b", "c", "d"], 2);
```

module.exports = {
getStringLength
};
With `esModuleInterop` set to `true`:

// @filename: index.ts
import utils from "./utilFunctions";
```ts twoslash
// @noErrors
// @showEmit
// @esModuleInterop
// @module: commonjs
import * as fs from "fs";
import _ from "lodash";

const count = utils.getStringLength("Check JS");
fs.readFileSync("file.txt", "utf8");
_.chunk(["a", "b", "c", "d"], 2);
```

This won't work because there isn't a `default` object which you can import. Even though it feels like it should.
For convenience, transpilers like Babel will automatically create a default if one isn't created. Making the module look a bit more like:
_Note_: You can make JS emit terser by enabling [`importHelpers`](#importHelpers):

```js
// @filename: utilFunctions.js
const getStringLength = str => str.length;
const allFunctions = {
getStringLength
};
```ts twoslash
// @noErrors
// @showEmit
// @esModuleInterop
// @importHelpers
// @module: commonjs
import * as fs from "fs";
import _ from "lodash";

module.exports = allFunctions;
fs.readFileSync("file.txt", "utf8");
_.chunk(["a", "b", "c", "d"], 2);
```

Turning on this compiler flag will also enable [`allowSyntheticDefaultImports`](#allowSyntheticDefaultImports).
Enabling `esModuleInterop` will also enable [`allowSyntheticDefaultImports`](#allowSyntheticDefaultImports).
1 change: 1 addition & 0 deletions packages/tsconfig-reference/scripts/tsconfigRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const recommended: CompilerOptionName[] = [
"strictFunctionTypes",
"noImplicitThis",
"noImplicitAny",
"esModuleInterop",
];

type RootProperties = "files" | "extends" | "include" | "exclude";
Expand Down

0 comments on commit 2e3a872

Please sign in to comment.