TypeScript 2.1: External Helpers Library

In some cases, the TypeScript compiler will inject helper functions into the generated output that are called at run-time. Each such helper function emulates the semantics of a specific language feature that the compilation target (ES3, ES5, ES2015, …) doesn't support natively.

Currently, the following helper functions exist in TypeScript:

  • __extends for inheritance
  • __assign for object spread properties
  • __rest for object rest properties
  • __decorate, __param, and __metadata for decorators
  • __awaiter and __generator for async/await

A typical use case for an ES2015 class with an extends clause is a React component like the following:

import * as React from "react";

export default class FooComponent extends React.Component<{}, {}> {
    render() {
        return (
            <div>Foo</div>
        );
    }
}

The TypeScript compiler will emit the following JavaScript code if you target ES5, where neither class nor extends are supported:

"use strict";
var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var React = require("react");
var FooComponent = (function (_super) {
    __extends(FooComponent, _super);
    function FooComponent() {
        return _super.apply(this, arguments) || this;
    }
    FooComponent.prototype.render = function () {
        return (React.createElement("div", null, "Foo"));
    };
    return FooComponent;
}(React.Component));
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = FooComponent;

While this approach works fine for a simple example like this, it has a huge disadvantage: The __extends helper function is injected into every file of the compilation that uses a class with an extends clause. That is, the helper is emitted for every class-based React component in your application.

For a medium-sized application with dozens or hundreds of React components, that's a lot of repetitive code just for the __extends function. That results in a noticeably bigger bundle size, which leads to longer download times.

This problem is only amplified when other helpers are emitted as well. I previously wrote about how TypeScript 2.1 downlevels async/await to ES3/ES5. The __awaiter and __generator helpers are huge and contribute significantly to bigger bundle sizes. Remember, they're injected into every file that uses the async/await keywords:

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments)).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t;
    return { next: verb(0), "throw": verb(1), "return": verb(2) };
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [0, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};

The --noEmitHelpers Flag

With version 1.5, TypeScript shipped the --noEmitHelpers flag. When this compiler option is specified, TypeScript won't emit any helper functions in the compiled output. This way, the bundle size goes down — potentially by a lot.

Here's the React component from before again, compiled with the --noEmitHelpers flag:

"use strict";
var React = require("react");
var FooComponent = (function (_super) {
    __extends(FooComponent, _super);
    function FooComponent() {
        return _super.apply(this, arguments) || this;
    }
    FooComponent.prototype.render = function () {
        return (React.createElement("div", null, "Foo"));
    };
    return FooComponent;
}(React.Component));
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = FooComponent;

Note that the call to __extends is still there. After all, it's required to make the React component work. If you use the --noEmitHelpers flag, it is your responsibility to provide all the helper functions needed! TypeScript assumes they'll be available at run-time.

However, it's cumbersome to keep track of all these helper functions manually. You have to check which ones your application needs and then somehow make them available within your bundle. Not fun at all! Luckily, the TypeScript team came up with a better solution.

The --importHelpers Flag and tslib

TypeScript 2.1 introduces a new --importHelpers flag which causes the compiler to import helpers from tslib, an external helpers library, rather than to inline them into each file. You can install and version tslib just like any other npm package:

npm install tslib --save

Let's compile our React component again, this time with the --importHelpers flag:

"use strict";
var tslib_1 = require("tslib");
var React = require("react");
var FooComponent = (function (_super) {
    tslib_1.__extends(FooComponent, _super);
    function FooComponent() {
        return _super.apply(this, arguments) || this;
    }
    FooComponent.prototype.render = function () {
        return (React.createElement("div", null, "Foo"));
    };
    return FooComponent;
}(React.Component));
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = FooComponent;

Notice the require("tslib") call in line 2 and the tslib_1.__extends call in line 5. There no longer is a helper function inlined into this file. Instead, the __extends function is imported from the tslib module. This way, each helper is only included once and you're no longer punished for using extends and async/await in many files.

Use the coupon code LAUNCHDAY for $10 off!

Learn ES6

3 Comments

Renato Cazangi

This is a good improvement. However, because I use browserify with absolute paths, it ended up having the opposite result: my bundle became bigger ;)

So folks in the same situation, keep this in mind and see if the trade off is worth it for your particular scenario.

Neek Sandhu

I use rollup with TypeScript plugin, it takes care of literally everything without any extra help from the user.

GiuseppePiscopo

Thanks for hint. I'm trying this in a webpack 2 (2.1.0-beta.25) build, with UglifyJs plugin setup, and 2 entry points: a main one for my actual application code and a vendor one for common third party libraries & framework. I can see that with this trick now compiled main.js only refers to some kind of global __extends. Instead vendor.js (the larger one, of course) is not affected at all by this technique. I guess that's because vendor code already comes precompiled in JS, so nothing a tsconfig.json could affect. Is there any way to apply such optimization to vendor code as well? Tnx