Module references in TypeScript

ts (TypeScript) can be used in node environment and web environment, or before the es module comes out. Most of the packages follow commonjs, and most of these packages that follow commonjs still exist in nodejs, which is the reason why nodejs has not fully supported esm. So how is ts compatible with commonjs and esm packages?

If you don’t use ts frequently or have not used ts in nodejs, then you will be surprised to see the official recommendation of ts. It is actually a mixed use of import and require.

Review cjs and esm

We need to understand why ts has such a unique recommended writing method, first review the related knowledge of commonjs and esm, and consider their interchangeability in ts

We have a commonjs file

// my-module.js
module.exports = { foo, bar }

Two ways to introduce and deconstruct, there seems to be no difference

// index.ts
// cjs
const { foo, bar } = require('my-module')
// esm
import { foo, bar } from 'my-module'

But let’s look at the following group

// module
export const foo = 1
export const bar = 2
export default () => {}

// esm
import { foo } from 'module'
import func from 'module'`
// module
module.exports = {
  foo: 1,
  bar: 2,
  default: () => {}
}
// cjs
const module = require('module')
const foo = module.foo
const func = module.default

Therefore, if we all use the default function, we need to operate module.default in commonjs to get it. For example, considering interoperability, in react.

import React from 'react'

// It is equivalent to only importing the default attribute
const {default: React} = require('react')

Therefore, before 2018, we were using ts to introduce React in order to maintain consistency. We would write import * as React from’react’ to get all the content in module.exports.

Therefore, when importing commonjs modules, ts can also use import * in addition to require mixed writing

typescript-import-as-vs-import-require

EsModuleInterop

ts2.7 has a configuration of esModuleInterop that supports import d from “cjs”

support-for-import-d-from-cjs-from-commonjs-modules-with—esmoduleintero

Let’s briefly look at how ts achieves compatibility and consistency.

// common.js
module.exports = {
  default: function greeter(person) {
    return "Hello, " + person;
  },
  person: "common"
};

// esm.js
export default function greeter(person) {
  return "Hello, " + person;
}

export const person = "esm";

// index.ts
import common from "./common";
const common1 = require("./common");
import * as common2 from "./common";

import esm from "./esm";
const esm1 = require("./esm");
import * as esm2 from "./esm";

console.log(common, common1, common2);
console.log(esm, esm1, esm2);

Code compiled when esModuleInterop is false.

"use strict";
exports.__esModule = true;
var common_1 = require("./common");
var common1 = require("./common");
var common2 = require("./common");
var esm_1 = require("./esm");
var esm1 = require("./esm");
var esm2 = require("./esm");
console.log(common_1["default"], common1, common2);
console.log(esm_1["default"], esm1, esm2);

//[Function: greeter] { default: [Function: greeter], person: 'common' } { default: [Function: greeter], person: 'common' }
//[Function: greeter] { default: [Function: greeter], person: 'esm' } { default: [Function: greeter], person: 'esm' }

It can be observed that import common from “./common”; the input result is only the default method, which is different from the output result of const common1 = require(“./common”); and cannot maintain consistency. This is also mentioned earlier Why do we need to import * or import and require are in the same equation.

Compiled code when esModuleInterop is true.

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
exports.__esModule = true;
var common_1 = __importDefault(require("./common"));
var common1 = require("./common");
var common2 = __importStar(require("./common"));
var esm_1 = __importDefault(require("./esm"));
var esm1 = require("./esm");
var esm2 = __importStar(require("./esm"));
console.log(common_1["default"], common1, common2);
console.log(esm_1["default"], esm1, esm2);

//{ default: [Function: greeter], person: 'common' } { default: [Function: greeter], person: 'common' } { default: { default: [Function: greeter], person: 'common' },
  person: 'common' }
//[Function: greeter] { default: [Function: greeter], person: 'esm' } { default: [Function: greeter], person: 'esm' }

If esModuleInterop is true, we can see that __importDefault and __importStar are added to determine whether the imported module is an esm module and then encapsulated, import common from “./common”; the input result is only the default method, and const common1 = require( “./common”); The output results are consistent.

Then why ts does not directly recommend the const common1 = require(“./common”) method?

We can get the result from the description in tslint’s no-var-requires, because amd and commonjs require is written in a specific environment, and it is difficult to perform static analysis.

Summary

  1. The project +esModuleInterop after ts2.7 does not need to consider some compatibility of the imported package
  2. Even for projects before ts2.7, it’s better not to use import and require in a mixed way, use import *, because esm will eventually be popular

Leave a Reply