The Typescript compiler compiles enums to functions, and it does so in a seemingly peculiar way. What are the benefits?
Note:
This question is not opinion-based. I am looking for objective, verifiable ways in which the compiled JavaScript code is better than the trivial option. I am not asking about the thinking process of the designers -- we cannot read their minds.
This question is not a duplicate. At least not a duplicate of the following two questions:
Compile an enum in TypeScript - This question is asking what is the compiled JavaScript, not why it is compiled that way.
Why does the TypeScript transpiler compile enums into dictionary lookups instead of simple objects? - This question is specifically asking about the following expression: Enum[Enum["None"] = 0] = "None"
, while my question is about the function aspects of the compiled code.
Suppose I define the following enum in TypeScript:
enum Color
{
Red,
Green,
Blue
}
This compiles to the following IIFE:
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
Why do we need the IIFE? Furthermore, since Color
is presumably known to be undefined
, and hence falsy, Color || (Color = {})
is seemingly guaranteed to be evaluated to Color = {}
. So what is the point of this expression?
I would presume an equivalent yet simpler implementation would look something like this:
var Color = {}; // why not let?
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
What is wrong with this approach?
A TypeScript enum
declaration creates the equivalent of a namespace
that holds both the enum members, as well as the types of these members. Your Color
enum is quite similar to the following:
namespace Color {
export const Red = 0;
export type Red = typeof Red;
export const Green = 1;
export type Green = typeof Green;
export const Blue = 2;
export type Blue = typeof Blue;
}
type Color = typeof Color[keyof typeof Color];
(although the above doesn't provide reverse mappings; these can also be emulated but to do so would be too much of a digression here.)
Furthermore, TypeScript supports namespace merging, where multiple namespace
s of the same name are combined. A namespace is "open" in this sense:
namespace Foo {
export const x = "abc";
}
namespace Foo {
export const y = Math.PI;
}
console.log(
Foo.x.toUpperCase(), Foo.y.toFixed(2)
); // "ABC", "3.14"
That means you can also merge enums:
enum Bar {
x = 1
}
enum Bar {
y = 2
}
or even merge namespace
s and enum
s together:
namespace Bar {
export function f() { console.log(Bar.x, Bar.y) }
}
Bar.f(); // 1, 2
In order for that to work in JavaScript, it means that a variable with the name of the namespace/enum must exist, but if it already exists you just want to modify it and not replace it. That means you don't want to assign anything to it at all. Hence output like:
var Bar;
(function (Bar) {
Bar[Bar["x"] = 1] = "x";
})(Bar || (Bar = {}));
(function (Bar) {
Bar[Bar["y"] = 2] = "y";
})(Bar || (Bar = {}));
(function (Bar) {
function f() { console.log(Bar.x, Bar.y); }
Bar.f = f;
})(Bar || (Bar = {}));
Each block will use an existing Bar
value if it is already defined, otherwise it will assign a fresh empty object to Bar
and use that instead. The IIFE is presumably for scoping, to keep unexported namespace variables private (including var
delcarations so block scope wouldn't be appropriate either, even if your target runtime supports it).
So there you go; an enum
is implemented as a namespace
and namespace
s can be merged, so you get IIFEs.