microsoft / microsoft/TypeScript

Reduce overhead and indirection in enum and namespace code generation

Open
#54,244 7 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Awaiting More Feedback Suggestion
Dominant language
Go
Stars
111k
Forks
14.4k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

Suggestion

🔍 Search Terms

enum emit

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • 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.)
  • This feature would agree with the rest of TypeScript's 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.

Suppose we have this code:

namespace Foo {
    export function bar() { return 1 }
}

enum Foo {
    One,
    Two,
    Three,
}

Currently, this results in the following emit:

"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):

"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.

"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

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 issues 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:

enum FooEnum {
    ONE,
    TWO,
    THREE
}

console.log(FooEnum.ONE, FooEnum[0])

The current emit when targeting modules is this:

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):

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:

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:

var o=0,l="ONE";console.log(o,l);

Adding one more pass (--compress passes=3) allows it to complete the enum inlining:

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

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:

export enum Foo {
    One,
    Two,
    Three,
}

The code it generates is this:

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):

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:

// 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:

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:

// 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"};

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No repository file, test, or compiler entry point is named. Start by locating the enum and namespace emit implementation and its existing tests, then compare generated output for the examples in the issue; done means the proposed lower-overhead output preserves the stated observable behavior and passes the relevant emit tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
compilers, performance
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.