Why does TypeScript pack a class in an IIFE?
Asked Answered
T

2

18

Here is a TypeScript class:

class Greeter {
    public static what(): string {
        return "Greater";
    }

    public subject: string;

    constructor(subject: string) {
        this.subject = subject;
    }

    public greet(): string {
        return "Hello, " + this.subject;
    }
}

It is transpiled to IIFE when TS targets ES5:

var Greeter = /** @class */ (function () {
    function Greeter(subject) {
        this.subject = subject;
    }
    Greeter.what = function () {
        return "Greater";
    };
    Greeter.prototype.greet = function () {
        return "Hello, " + this.subject;
    };
    return Greeter;
}());

However, it generally works in the same way when it is presented as a constructor function. Which, of course, looks more JavaScriptish and handwritten :)

function Greeter(subject) {
    this.subject = subject;
}
Greeter.what = function () {
    return "Greater";
};
Greeter.prototype.greet = function () {
    return "Hello, " + this.subject;
};

Usage:

Both blocks of code work in the same way:

Greater.what();  // -> "Greater"
var greater = new Greater("World!");
greater.greet(); // -> "Hello, World!

What is the benefit or motives to pack it in IIFE?

I made a naive benchmark:

console.time("Greeter");
for(let i = 0; i < 100000000; i++) {
    new Greeter("world" + i);
}
console.timeEnd("Greeter");

It showed virtually the same instantiation speed. Of course, we cannot expect any difference, because the IIFE is resolved only once.

I was thinking that maybe it is because of closure, but the IIFE doesn't take arguments. It must not be a closure.

Transpire answered 11/5, 2019 at 1:5 Comment(1)
@charlietfl TS doesn't use the IIFE to hide private fields, actually. In JS they are all public.Smelly
S
15

TypeScript will pass arguments to the IIFE in cases where there is inheritance between classes. For example, the closure below is used when Greeter extends a BaseGreeter class:

var Greeter = /** @class */ (function (_super) {
    // __extends is added by the TS transpiler to simulate inheritance
    __extends(Greeter, _super);
    function Greeter(subject) {
        var _this = _super.call(this) || this;
        _this.subject = subject;
        return _this;
    }
    Greeter.What = function () {
        return "Greater";
    };
    Greeter.prototype.greet = function () {
        return "Hello, " + this.subject;
    };
    return Greeter;
}(BaseGreeter));
Stapler answered 11/5, 2019 at 1:38 Comment(0)
A
12

It's done to preserve native class behavior in edge cases like this, where someone tries to use class Greeter before it's defined:

// this is javascript code, not TypeScript

console.log(Greeter.What());

class Greeter {
}

Greeter.What = function What() {
    return "Greater";
}

With native class implementation, this should print ReferenceError: Greeter is not defined.

When transpiled and wrapped in IIFE, the result is close enough: TypeError: Cannot read property 'What' of undefined.

Without IIFE, unwrapped function is hoisted and name Greeter is in scope before it's defined, so the different error is produced: TypeError: Greeter.What is not a function

Note that IIFE is not used to hide private instance or class properties because it's not necessary anyway. When transpiled, instance properties are assigned as properties for this inside the constructor, and static properties are assigned as properties of Greeter object - no variables are created.

Afterburning answered 11/5, 2019 at 1:29 Comment(3)
A var won't get hoistedClubhaul
Yes, the other answer is the correct one - you have to be able to introduce a name to refer to a base class, because for example base class might be a mixin. IIFE is the only way to safely introduce a name which will not conflict with anything in the outer scope.Afterburning
I think both answers are right about different things. Yeah, var isn't hoisted but function Greeter by itself (as the OP showed) would be, which I thought was artem's point here. Also appreciate the point that TS doesn't actually use this for private fields.Smelly

© 2022 - 2024 — McMap. All rights reserved.