Repository: TypeScript
Issue: Reduce overhead and indirection in enum and namespace code generation
Labels: Suggestion,Awaiting More Feedback

# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## 🔍 Search Terms

<!--
  💡 Did you know? TypeScript has over 2,000 open suggestions!
  🔎 Please search thoroughly before logging new feature requests as most common ideas already have a proposal in progress.
  The "Common Feature Requests" section of the FAQ lists many popular requests: https://github.com/Microsoft/TypeScript/wiki/FAQ#common-feature-requests

  Replace the text below:
-->

enum emit

## ✅ Viability Checklist

<!--
   Suggestions that don't meet all these criteria are very, very unlikely to be accepted.
   We always recommend reviewing the TypeScript design goals before investing time writing
   a proposal for ideas outside the scope of the project.
-->
My suggestion meets these guidelines:

* [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [ ] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
* [x] This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).


## ⭐ Suggestion

> Note: this is a partial dupe of https://github.com/microsoft/TypeScript/issues/27604 but has a few key differences:
> 1. It proposes a non-breaking change instead.
> 2. It extends the change to namespaces as well.
> 3. It in theory isn't tree-shakeable, but can still be optimized out by optimizers.

<!-- A summary of what you'd like to see added or changed -->
Suppose we have this code:

```ts
namespace Foo {
    export function bar() { return 1 }
}

enum Foo {
    One,
    Two,
    Three,
}
```

Currently, this results in [the following emit](https://www.typescriptlang.org/play#code/HYQwtgpgzgDiDGEAEAxA9mpBvAUE-SEAHjGgE4AuSAZgK7DwUCWawSARiGQBQCU2SMhAq0ybAIxIAvjhk4IwWmFQZseAgHlgEADTr8AFQDuaPQSQGAFkN2ygA):

```js
"use strict";
var Foo;
(function (Foo) {
    function bar() { return 1; }
    Foo.bar = bar;
})(Foo || (Foo = {}));
(function (Foo) {
    Foo[Foo["One"] = 0] = "One";
    Foo[Foo["Two"] = 1] = "Two";
    Foo[Foo["Three"] = 2] = "Three";
})(Foo || (Foo = {}));
```

A much better emit would be this (comments explaining each bit):

```js
"use strict";
// Forward declare the namespace
// In modules and functions, this should just be `var Foo = {}`
var Foo = Foo || {};

// `namespace Foo {`
// Deduplicate like you already do with `let`/`const` when targeting ES5
function bar() { return 1; }
Foo.bar = bar;
// `}`

// `enum Foo {`
// `  One,`
Foo.One = 0;
Foo[0] = "One";
// `  Two,`
Foo.Two = 1;
Foo[1] = "Two";
// `  Three,`
Foo.Three = 2;
Foo[2] = "Three";
// `}`
```

This would carry the same observable semantics it currently does (complete with `Object.prototype` observability).

You may be able to go one step further and just build the object literal directly. This of course *is* observable (`Object.prototype` setters would not be invoked), but would result in ideal code for most cases. You'd only be able to do this for cases like modules, though.

```js
"use strict";
// Hoist `bar`
function bar() { return 1; }
var Foo = {
// `namespace Foo {`
    bar: bar,
// `}`

// `enum Foo {`
// `One,`
    One: 0,
    0: "One",
// `Two,`
    Two: 1,
    1: "Two",
// `Three,`
    Three: 2,
    2: "Three",
// `}`
}
```

Do want to note that while this can technically be larger for a few enums, it'll only be that for a few, and it'll almost certainly be a wash after compression as well.

## 📃 Motivating Example

<!--
  If you were announcing this feature in a blog post, what's a short explanation that shows
  a developer why this feature improves the language?
-->

This reduces namespace and enum code gen overhead significantly and also make it much easier to optimize for both engines (on startup) and optimizer tools.

Currently, Terser has [two](https://github.com/terser/terser/issues/1064) [issues](https://github.com/terser/terser/issues/1177) around TypeScript enum generation, and this would serve to fix both.

The first issue linked, https://github.com/terser/terser/issues/1064, features this code:

```ts
enum FooEnum {
    ONE,
    TWO,
    THREE
}

console.log(FooEnum.ONE, FooEnum[0])
```

The current emit when targeting modules is this:

```js
var FooEnum;
(function (FooEnum) {
    FooEnum[FooEnum["ONE"] = 0] = "ONE";
    FooEnum[FooEnum["TWO"] = 1] = "TWO";
    FooEnum[FooEnum["THREE"] = 2] = "THREE";
})(FooEnum || (FooEnum = {}));
console.log(FooEnum.ONE, FooEnum[0]);
```

Terser, with `--module --mangle --compress passes=2`, minifies it to this (whitespace added for clarity):

```js
var E;
!function(E){
    E[E.ONE=0]="ONE",
    E[E.TWO=1]="TWO",
    E[E.THREE=2]="THREE"
}(E||(E={})),
console.log(E.ONE,E[0]);
```

My proposed emit would be this:

```js
var FooEnum = {};
FooEnum.ONE = 0;
FooEnum[0] = "ONE";
FooEnum.TWO = 1;
FooEnum[1] = "TWO";
FooEnum.THREE = 2;
FooEnum[2] = "THREE";

console.log(FooEnum.ONE, FooEnum[0])
```

Terser with the same settings compresses it to just this:

```js
var o=0,l="ONE";console.log(o,l);
```

Adding one more pass (`--compress passes=3`) allows it to complete the enum inlining:

```js
console.log(0,"ONE");
```

> Worth noting that Terser by default only performs one pass. This should probably be called out in whatever blog post for visibility.

## 💻 Use Cases

<!--
  What do you want to use this for?
  What shortcomings exist with current approaches?
  What workarounds are you using in the meantime?
-->
The current approach just quite frankly is extremely difficult to optimize for. Not only is it bloated, but it's also difficult to detect for minification purposes - the motivating example elaborates on this further.

It'd also boost startup speed, even absent the minifier optimizations, since it's only setting properties, not also going through the ceremony of IIFEs plus `Enum || (Enum = {})`.

Also, consider this code:

```ts
export enum Foo {
    One,
    Two,
    Three,
}
```

The code it generates is this:

```js
export var Foo;
(function (Foo) {
    Foo[Foo["One"] = 0] = "One";
    Foo[Foo["Two"] = 1] = "Two";
    Foo[Foo["Three"] = 2] = "Three";
})(Foo || (Foo = {}));
```

Terser, with `--module --mangle --compress passes=2`, minifies it to this (whitespace added for clarity):

```js
export var Foo;
!function(o){
    o[o.One=0]="One",
    o[o.Two=1]="Two",
    o[o.Three=2]="Three"
}(Foo||(Foo={}));
```

My proposed emit would be this:

```js
// Proposed
export var Foo = {};
Foo.One = 0;
Foo[0] = "One";
Foo.Two = 1;
Foo[1] = "Two";
Foo.Three = 2;
Foo[2] = "Three";

// Ideal
export var Foo = {
    One: 0,
    0: "One",
    Two: 1,
    1: "Two",
    Three: 2,
    2: "Three",
};
```

Once https://github.com/terser/terser/issues/1389 gets resolved (it's about the `export` specifically), Terser *should* minify the proposed one to this:

```js
export var Foo={One:0,0:"One",Two:1,1:"Two",Three:2,2:"Three"};
```

> The "ideal" code would just minify to this without the complicated multi-pass processing.

With `esbuild --minify`, it's not as sophisticated, but benefits are still apparent. Here are the current and new emits side-by-side with it, to show the difference:

```js
// Current
export var Foo;(function(e){e[e.One=0]="One",e[e.Two=1]="Two",e[e.Three=2]="Three"})(Foo||(Foo={}));
// Proposed
var e={};e.One=0,e[0]="One",e.Two=1,e[1]="Two",e.Three=2,e[2]="Three";export{e as Foo};
// Ideal
export var Foo={One:0,0:"One",Two:1,1:"Two",Three:2,2:"Three"};
```