problem introduction
Many react
users may encounter such a problem when migrating from JS
to TS
:
JS
introduces react
like this:
// js
import React from 'react'
And TS
is like this:
// ts
import * as React from 'react'
If you directly change JS
TS
the same way, when @types/react
is installed, the editor will throw an error: This module is declared with " export
=" and can only be used with " 06204b7fd69e00 " when using the " esModuleInterop
" flag. Used with default imports.
According to the prompt, set compilerOptions.esModuleInterop
to true
in tsconfig.json
, and the error will disappear.
To figure out the cause of this problem, you first need to know the module system of JS
. There are three commonly used module systems of JS
:
CommonJS
(hereinafter referred to ascjs
)ES module
(hereinafter referred to asesm
)UMD
( AMD
is used less now, so ignore it)
Compilers such as babel
and TS
prefer cjs
. By default, esm
written in the code will be converted to cjs
by babel
and TS
. I'm guessing the reasons for this are as follows:
cjs
appeared earlier thanesm
, so there are already a lot ofnpm
libraries based oncjs
(the number is much higher thanesm
), such asreact
cjs
has a very mature, popular and highly usedruntime:Node.js
, whileesm
ofruntime
currently supports very limited (the browser side requires an advanced browser, andnode
requires some weird configuration and modification of the file suffix)- There are many
npm
libraries based onUMD
,UMD
is compatible withcjs
, but becauseesm
is static,UMD
cannot be compatible withesm
Back to the question above. Open react
of the index.js
library:
You can see that react
is based on cjs
, which is equivalent to:
module.exports = {
Children: Children,
Component: Component
}
And in index.ts
, write a paragraph
import React from "react";
console.log(React);
By default, the code compiled with tsc
is:
"use strict";
exports.__esModule = true;
var react_1 = require("react");
console.log(react_1["default"]);
Obviously, the printed result is undefined
, because there is no default
and this attribute in react
of module.exports
. Therefore, subsequent acquisitions of React.createElement
and React.Component
will naturally report errors.
The problem derived from this question is actually that most of the existing third-party libraries are mostly written with UMD / cjs
(or, they use their compiled products, and the compiled products are generally cjs
), but Now the front-end code is basically written with esm
, so esm
and cjs
need a set of rules to be compatible.
esm
importesm
- Both sides will be converted to
cjs
- Write in strict accordance with the standard of
esm
, generally there will be no problems
- Both sides will be converted to
esm
importcjs
- It is most common to refer to third-party libraries, such as
react
in the example in this article - compatibility problem arises because
esm
has the concept ofdefault
, butcjs
does not. Any exported variable appears tocjs
as an attribute on the objectmodule.exports
, and theesm
export ofdefault
is only thecjs
attribute onmodule.exports.default
- The importer
esm
will be converted tocjs
- It is most common to refer to third-party libraries, such as
cjs
importesm
(generally not used like this)cjs
importcjs
- will not be processed by the compiler
- Write strictly according to the standard of
cjs
, there will be no problem
TS default compilation rules
The translation rules of TS
for import
variables are:
// before
import React from 'react';
console.log(React)
// after
var React = require('react');
console.log(React['default'])
// before
import { Component } from 'react';
console.log(Component);
// after
var React = require('react');
console.log(React.Component)
// before
import * as React from 'react';
console.log(React);
// after
var React = require('react');
console.log(React);
can be seen:
- For
import
importing the default exported module,TS
will read the abovedefault
attribute when reading this module - For
import
importing non-default exported variables,TS
will read the corresponding properties of this module - For
import *
,TS
will directly read the module
The translation rules of TS
and babel
to export
variables are: (the code is simplified)
// before
export const name = "esm";
export default {
name: "esm default",
};
// after
exports.__esModule = true;
exports.name = "esm";
exports["default"] = {
name: "esm default"
}
can be seen:
- For a variable of
export default
,TS
will put it on themodule.exports
property ofdefault
- For the variable of
export
,TS
will put it on the property ofmodule.exports
corresponding to the variable name - Add an additional attribute of
__esModule: true
tomodule.exports
to tell the compiler that this is originally aesm
module
Compilation rules after TS opens esModuleInterop
Back to the title, the property esModuleInterop
defaults to false
. After changing to true
, the translation rules of TS
for import
will undergo some changes (the rules of export
will not change):
// before
import React from 'react';
console.log(React);
// after 代码经过简化
var react = __importDefault(require('react'));
console.log(react['default']);
// before
import {Component} from 'react';
console.log(Component);
// after 代码经过简化
var react = require('react');
console.log(react.Component);
// before
import * as React from 'react';
console.log(React);
// after 代码经过简化
var react = _importStar(require('react'));
console.log(react);
As you can see, for the default import and the namespace(*)
import, TS
uses two helper
functions to help
// 代码经过简化
var __importDefault = function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
var __importStar = function (mod) {
if (mod && mod.__esModule) {
return mod;
}
var result = {};
for (var k in mod) {
if (k !== "default" && mod.hasOwnProperty(k)) {
result[k] = mod[k]
}
}
result["default"] = mod;
return result;
};
first look at __importDefault
. What it does is:
- If the target module is
esm
, return the target module directly; otherwise, hang the target module ondefalut
of an object, and return the object.
such as the above
import React from 'react';
// ------
console.log(React);
After compiling, translate layer by layer:
// TS 编译
const React = __importDefault(require('react'));
// 翻译 require
const React = __importDefault( { Children: Children, Component: Component } );
// 翻译 __importDefault
const React = { default: { Children: Children, Component: Component } };
// -------
// 读取 React:
console.log(React.default);
// 最后一步翻译:
console.log({ Children: Children, Component: Component })
In this way, the modue.exports
of the react
module has been successfully obtained.
__importStar
again. What it does is:
- If the target module is
esm
, return the target module directly. otherwise - Move all attributes except
default
on the target module toresult
- Hang the target module itself on
result.default
(Similar to the above __importDefault
, the layer-by-layer translation analysis process is skipped)
Rules for babel compilation
The default translation rule of babel
is similar to the case of TS
opening esModuleInterop
, and it is also handled by two helper
functions.
// before
import config from 'config';
console.log(config);
// after
"use strict";
var _config = _interopRequireDefault(require("config"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
console.log(_config["default"]);
// before
import * as config from 'config';
console.log(config);
// after
"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
var config = _interopRequireWildcard(require("config"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
console.log(config);
_interopRequireDefault
similar to __importDefault
_interopRequireWildcard
similar to __importStar
webpack
module handling
In general development, babel
and TS
will be used together with webpack
. Generally there are two ways:
ts-loader
babel-loader
If ts-loader
is used, then webpack
will hand over the source code to tsc
for compilation, and then process the compiled code. After compiling with tsc
, all modules will become cjs
, so babel
will not be processed, and directly handed over to webpack
to process modules in the way of cjs
. ts-loader
actually calls the tsc
command, so the tsconfig.json
configuration file is required
If babel-loader
is used, then webpack
will not call tsc
, and tsconfig.json
will be ignored. Instead, use babel
to compile ts
file directly. This compilation process is much lighter than calling tsc
, because babel
will simply remove all ts
-related code without type checking. Generally in this case, a ts
module is processed by two babel
of @babel/preset-env
and @babel/preset-typescript
of preset
. What the latter does is very simple, it just removes all ts
related codes and does not process modules, while the former converts esm
to cjs
. babel7
began to support compiling ts
, so the existence of tsc
was weakened. webpack
of babel-loader
actually calls the babel
command, which requires the babel.config.js
configuration file
However, when webpack
of babel-loader
called babel.transform
, it passed such a caller
option:
resulting in babel
retaining esm
of import export
tsc
, babel
can esm
compiled into cjs
, but cjs
only node
to run under the environment, and webpack
own set of modules mechanism to deal with cjs
esm
AMD
UMD
other kinds of modules and provides a module runtime
. Therefore, the code that needs to run in the browser eventually needs webpack
for modular processing
For cjs
to refer to esm
, the compilation mechanism of webpack
is special:
// 代码经过简化
// before
import cjs from "./cjs";
console.log(cjs);
// after
var cjs = __webpack_require__("./src/cjs.js");
var cjsdefault = __webpack_require__.n(cjs);
console.log(cjsdefault.a);
// before
import esm from "./esm";
console.log(esm);
// after
var esm = __webpack_require__("./src/esm.js");
console.log(esm["default"]);
where __webpack_require__
is similar to require
and returns the module.exports
object of the target module. __webpack_require__.n
This function receives a parameter object and returns an object. The a
property of the returned object (I don't know why the property is named a
) will be set as the parameter object. So console.log(cjs)
of the above source code will print cjs.js
of module.exports
Since webpack
provides a runtime
for the module, the webpack
processing module is very free for webpack
itself, and it is enough to inject a variable representing module
require
exports
into the module closure
Summarize:
At present, many commonly used packages are developed based on cjs / UMD
, and the front-end code is generally written by esm
, so the common scenario is that esm
imports the library of cjs
. However, due to the conceptual difference between esm
and cjs
, the biggest difference is that esm
has the concept of default
and cjs
does not, so there will be problems on default
.
TS
babel
webpack
have their own set of processing mechanisms to deal with this compatibility problem. The core idea is basically to add and read the attributes of default
.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。