1 Introduction to TypepScript Syntax Compilation and Helper Functions

Using TypepScript’s built-in tsc tool, you can translate ts source files into standard JavaScript code files. The configuration file tsconfig.json can be used to configure the specific scheme of tsc compilation.

The tsc compilation parameter target specifies the language standard version of the output, so in practice it can also be used as a tool to translate higher versions of ECMAScript source code to lower versions.

During the syntax translation process, additional helper functions may be needed for syntax degradation compatibility, such as compatibility with inheritance (_extends), expansion operators (__assign), asynchronous functions (__awaiter), etc.

1.1 tsc compilation results and helper functions examples and analysis

Here is a simple example.

For the index.ts file, the contents are as follows.

1
2
// index.ts
export * './common';

tsconfig.json is configured as follows.

1
2
3
4
5
6
7
{
    "compilerOptions": {
        "module": "commonjs",
        "outDir": "cjs",
        "target": "es2020"
    }
}

Then the output of tsc@4.7.4 is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// index.js
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
    for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./common"), exports);

You can see that the __createBinding and __exportStar helper functions are injected directly into the output of the commonjs specification, turning one line of code into 16 lines.

If there are many ts files that require the __exportStar helper function, this obviously results in a lot of duplicate injections in the output.

1.2 Compilation parameters for tsc noEmitHelpers

To solve this problem, TypeScript introduced the compilation parameter noEmitHelpers in 2015. When this parameter is turned on, tsc compilation results will no longer inject helper functions.

Let’s modify the tsconfig.json configuration as follows.

1
2
3
4
5
6
7
{
    "compilerOptions": {
        "noEmitHelpers": true,
        "module": "commonjs",
        "outDir": "cjs"
    }
}

The output of the index.ts file in the above example will look like this.

1
2
3
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("../common"), exports);

Without the helper function injection, the code is much simpler. However, there is still the use of helper function names, which are defined in the global functions by default.

In this case, you only need to maintain a file for defining the global helper functions used in the translation results.

For small and medium-sized projects, maintaining a file of helper functions used in the project is not too complicated, but there are problems such as synchronized updates. For large, complex projects, the maintenance cost and mental burden is more obvious. In addition, if a project introduces multiple external tool libraries for tsc compilation and output, the helper functions will still have to be introduced repeatedly.

1.3 Compilation parameters for tsc importHelpers

tsc soon introduced the new variant parameter importHelpers and the tslib library to address the issue of helper function uniformity.

The importHelpers parameter allows each project to import helper functions from tslib once, instead of including them in every file.

Modify the tsconfig.json configuration as follows.

1
2
3
4
5
6
7
{
    "compilerOptions": {
        "importHelpers": true,
        "module": "commonjs",
        "outDir": "cjs"
    }
}

The output of the index.ts file in the above example will look like this.

1
2
3
4
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
tslib_1.__exportStar(require("../common"), exports);

As you can see, all helper functions are now imported from the tslib library. This allows for the result that only one copy of the helper function will exist.

1.4 tsc and auxiliary functions summary

To summarize briefly, the tsc compilation results in three options for handling helper functions.

  1. inject the implementation of the helper function in every file that requires it.
  2. use --noEmitHelpers: only use helper functions but do not inject their implementation, maintain the global helper functions by themselves.
  3. use -importHelpers: helper libraries are added to the project as separate modules, and the compiler imports them on demand.

Handling of babel and swc compilation results and helper functions

babel, swc and tsc are popular tools that compile and convert Javascript code written in high-level syntax for backward compatibility.

2.1 babel and @babel-runtime

babel can achieve similar effects to importHelpers by using the plugin @babel/plugin-transform-runtime, which uses the helper library @babel/runtime. Here is an example of the plugin’s configuration in the babel configuration file .babelrc.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "version": "7.0.0"
      }
    ]
  ]
}

As you can see, the helpers parameter has a similar function to importHelpers in tsc.

There is also a corejs parameter, which sets whether to use the core-js library where necessary to achieve compatibility with lower language versions of high-level syntax standards.

You can refer to the official babel documentation for more information about the functionality of the plugin’s parameters.

2.2 swc and @swc/helpers

swc provides the compilation parameters externalHelpers and the @swc/helpers library to handle helper functions.

With the externalHelpers parameter turned on, export * from '. /common' will be compiled as follows.

1
2
3
4
5
6
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
var _exportStar = require("@swc/helpers/lib/_export_star.js").default;
_exportStar(require("./common"), exports);

You can see that the compilation result is very similar to tsc with importHelpers turned on. And furthermore, each helper function is introduced as a single file, so as not to introduce unused other helper functions as much as possible.

2.3 Why do I need core-js?

Compilers such as tsc will only rewrite high-level syntax during syntax compilation that can be implemented using the low-level syntax standard-compatible, using helper functions. Those that cannot be directly implemented by the low-level syntax (e.g., prototype extension methods like 'abcabca'.repalceAll('a', '')) are not handled and are output directly as is.

Both tslib, @babel/runtime and @swc/helpers contain only the helper functions needed for syntax translation, not the polyfill implementation for advanced syntax. The es6 source code of the tslib library is just over two hundred lines long.

For true full backward compatibility, a library like core-js is needed. core-js provides a number of polyfill implementations with advanced syntax.

As you can see, tsc and swc do not deal with polyfill, leaving it up to the user to decide how to do syntax-level backwards compatibility. babel, on the other hand, uses the corejs (based on the plugin babel-plugin-polyfill-corejs<2|3>) parameter of the plugin @babel/plugin-transform-runtime to decide how to inject the relevant syntax implementation.

So it can be said that only babel and its official plugin system provide a compilation scheme that is really as backward compatible as possible.

Due to the plugin-based architecture of babel, there are so many plugins that it is easy to get lost between them. For the same type of problem, you may have different solutions and options in different plugins. In general, you might not actively use the @babel/plugin-transform-runtime plugin, but introduce @babel/preset-env to configure the compilation options for babel. @babel/preset-env provides parameters such as corejs and useBuiltIns to enable custom introduction of core-js.

3 Compilation options for helper functions

As we summarized in subsection 1.4, there are three options for auxiliary functions. How should they be chosen?

In a real project, there is no single best option depending on the use and size of the project.

Generally speaking, under the current development model and tooling system, maintaining global helper functions on your own is not suitable for most projects. A tree-shaking based build removes code from the final output that is not actually used.

3.1 Options for private business class projects

For regular business projects, a complete final solution is required. For larger projects with complex business logic, it is recommended to use libraries such as tslib.

  • When considering the size of the output, it is recommended to turn on the helpers parameter and uniformly reference helper functions from libraries such as tslib.
  • Alternatively, you can introduce core-js in combination with the babel plugin to adapt to the lowest preset language version.
    • Hint: babel can be used to analyze which specific polyfill subfiles in core-js are actually needed based on the source code.
  • You can also decide how to introduce core-js on your own.
  • If the project needs to be compatible with a very low runtime environment, you can also consider bringing in the full core-js library directly.

The helpers parameter is turned off by default in several compilation tools, and actually injecting helper functions is irrelevant in most projects.

3.2 Public libraries for shared use

For public libraries published to npm, the priority is to adapt them to specific application scenarios as friendly as possible.

If the library is small, e.g. a few files, the output will contain very few helper functions. You can choose to inject helper functions by default.

If the project is large and complex and oriented towards Node.js-like projects, it is recommended to turn on the helper option. Introducing a library of helper functions will avoid a lot of duplication in the final project.

3.3 Loose: Avoid helper functions whenever possible

One of the purposes of some helper functions is to make the compiled logic as identical as possible to the high-level syntax. However, in many real-world scenarios, it is fine not to be completely consistent. Both babel and swc provide the loose parameter, which when turned on, the output can be less strict and some helper functions will not be injected anymore.

Take the following example.

1
2
const b = [1, , 2];
const a = [...b];

swc with the externalHelpers parameter turned on, in order to compile to the following.

1
2
3
4
5
6
7
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
var _toConsumableArray = require("@swc/helpers/lib/_to_consumable_array.js").default;
var b = [1, , 2];
var a = _toConsumableArray(b);

swc injects up to 6 helper functions in order to inject _toConsumableArray without the externalHelpers parameter turned on.

With the loose parameter turned on, the compiled result of swc is simplified as follows.

1
2
3
"use strict";
var b = [1, , 2];
var a = [].concat(b);

In the example above, the result of the variable a is somewhat different, but there may not be a difference in the final processing of the actual business logic.

3.4 ESM (ES Module): a future-oriented solution

Analyzing the role and type of helper functions, there are two main types: high-level syntax compatibility (target) and module loader adaptation (module).

If the compiled result no longer requires compatibility between these two, the helper function is not needed. Just change the output to the ESM latest specification approach.

In the case of tsc, the configuration in tsconfig.json can be changed to the following.

1
2
3
4
5
6
{
    "compilerOptions": {
        "target": "ESNext",
        "module": "ESNext"
    }
}

Thanks to the development of language standards, the ESM modularity standard solution is gaining popularity in the community at a very fast pace.

  • For non-Node.js oriented public libraries, they can be fully exported to the ESM model, leaving it up to the business application build process to decide exactly how to take it further.
  • For Node.js-oriented public libraries, the community is also moving closer to the ESM standard.
  • For business application development, the ESM module loading scheme is also the way forward.

It is important to note that.

  • The ESM solution no longer requires module loader helper functions, and is left to the final business builder (e.g. webpack, rollup, esbuild, etc.) to integrate and handle.
  • As long as the value of target is not ESNext, the introduction of helper functions is still unavoidable in order to handle the backward compatibility of high-level syntax.

4 Modularity in the Front End and the Development of the ESM (ES Modules) Standard

Before ES6(ECMAScript 2015), there was no modular loading standard for JavaScript. The open source community has precipitated well-known module loading schemes such as AMD, CommonJS, and CMD based on development practices in large projects.

4.1 UMD and ESM

For a long time, open source public libraries for multiple runtime environments in the community have been producing source code that follows the UMD specification in order to be compatible with both AMD and CommonJS.

With the standardization of ESM and the development of front-end engineering build tools, especially after rollup proposed an ESM-based tree shaking build optimization scheme, a large number of public libraries for browser-oriented applications started to be built. A large number of public libraries for browser-based applications started to provide ESM-standardized content while exporting UMD-compliant source code.

  • CommonJS(CJS) - Node.js
  • AMD - require.js
  • CMD - sea.js
  • System - System.js module loader and builder
  • UMD - Universal Module Definition, compatible with both the AMD specification, the CommonJS specification and the global variable approach.
  • UMD + ESM

4.2 CJS and ESM in Node.js

Node.js has been designed and implemented from the beginning with the CommonJS (CJS) specification. For a long time there was no better option for a Node.js-only application module.

Because of the vast differences between the ESM and CJS solutions, it is difficult for them to coexist. A Node.js application can load a very large number of external dependencies, which makes moving to an ESM standardized solution, whether it is a public library or a private business module, a large package. The main manifestations are.

  • Downward compatibility: changing the module loading scheme can result in downstream applications not being able to introduce and use it directly
  • External dependencies: In order to introduce and use other public library dependencies, it is not possible to use the ESM solution directly

Because of this historical baggage, ES Module has not been supported in Node.js, although it has been in the standard since ES6+.

The turning point came with ES2020, which added the Dynamic import() dynamic import specification to its language standard. In May 2020, Node.js officially enabled support for the ES Module modularity scheme with the release of 12.17.0 LTS.

In Node.js, the Dynamic import() syntax supports both CJS and ESM schemas. Thus, the backward compatibility baggage of being a public library is gone. Downstream applications of the CJS specification simply need to change to dynamically import to reference the dependencies of the ESM module.

Since then the ESM modularity solution has really started to gain popularity in the Node.js community. Many popular open source libraries started to release new versions without the CJS solution and only output the results of the ESM solution. Some of the more representative ones are chalk@5 and others.

Based on the large adoption of the CJS module loading scheme, the CJS and ESM schemes will continue to co-exist in front-end development applications for a long time, but will probably be completely replaced by the ESM standard in the future.