External Helpers Library in TypeScript
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
forasync
/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.
This article and 44 others are part of the TypeScript Evolution series. Have a look!