Repository: TypeScript
Issue: Proposal: Merge enum and const enum features
Labels: Suggestion,Needs Proposal

Currently there are two enumerable types specified in TypeScript: `enum` and `const enum`.

Both of them aren't bijective, i.e. they both don't provide the ability to cast them arbitrarily and unambiguously between `string`, `number` and `enum`.

After discussing this on Gitter with @jsobell and @masaeedu I'd like to propose the following:
1. merge both enumerable types into one: using `const enum` on constant index expressions and `enum` on variable enum expressions.
2. always return a `number` value when a `string` index expression is used on an `enum`.
3. allow for both, `number` and `string` values, to be used as index argument types on the proposed merged `enum` type.

This would solve some major problems with the current design. Currently ...
1. `enum` values cannot be converted to their `number` equivalent, only to their `string` representation.
2. `const enum` values cannot be converted to their `string` representation, only to their `number` equivalent.
   (This blocks `const enum` values from being used to serialize configuration settings into a commonly expected string value representation.)
3. `const enum` values can not be converted to `enum` values and vice versa.

The key to providing the missing functionality is type inference. At compile time, TSC is able to tell whether a string or a number value is provided in an `enum` index argument. It's also able to tell whether the index expression is a constant or a variable.

Given these prerequisites the compiler can easily decide ...
1. whether to use a `const enum` value or an `enum` in the generated code,
2. whether to return the numerical value or string representation of the enum in the generated code.

(Variable index expressions of type _any_ should be regarded as values of type `string`.This will result in maximum compatibility.)
#### So I'm proposing the following:
1. Using a `string` indexer expression on an `enum` shall return a `number` if the type of the L-value _isn't_ the enum itself:  `enum[:string] => number`.
2. Using a `string` indexer expression on an `enum` shall return an `enum` if the type of the L-value _is_ the enum itself:  `enum[:string] => E`.
3. Using a `number` indexer expression on an `enum` shall return a `string` if the type of the L-value _isn't_ the enum itself:  `enum[:number] => string`.
4. Using a `number` indexer expression on an `enum` shall return an `enum` if the type of the L-value _is_ the enum itself:  `enum[:number] => E`.
5. Using a `enum` indexer expression on the same `enum` type shall return a `number` if the type of the L-value _isn't_ the enum itself:  `enum[:enum] => number`. (During transpilation, this operation is practically redundant and may be cancelled from the output.)
6. Using a `enum` indexer expression on the same `enum` type shall return an `enum` if the type of the L-value _is_ the enum itself:  `enum[:enum] => E`. (During transpilation, this operation is practically redundant and may be cancelled from the output.)
7. Using a _constant_ `string` or `number` indexer expression on an `enum` shall emit a constant `number` value in JavaScript (i.e., this is the `const enum` equivalent).
8. Using a _variable_ `string` or `number` indexter expression on an `enum` shall emit an array indexer expression in JavaScript (i.e., this is the `enum` equivalent).

---

<br/>
#### So, given the above prerequisites, here are two examples, hopefully shedding some light upon my proposal:

<br/>
##### (A) Example illustrating type inference being used at compile time to decide whether to return a `number` or a `string` value from an enum:

The following TypeScript code:

``` ts
// TypeScript
enum E {a, b, c}
const iN :number = 0, iS1 :string = "b", iS2 :string = "2";

// assigning to primitive types
const s :string = E[iN];
const n1 :number = E[iS1];   // special treatment because index type is string
const n2 :number = E[iS2];   // special treatment because index type is string

// assigning to enum
const es :E = E[iN];
const en1 :E = E[iS1];   // special treatment because index type is string
const en2 :E = E[iS2];   // special treatment because index type is string
```

... should result in the following JavaScript compilation result:

``` js
// JavaScript
var E;
(function (E) {
    E[E["a"] = 0] = "a";
    E[E["b"] = 1] = "b";
    E[E["c"] = 2] = "c";
    E.toNumber = function (e)
                 { return IsNaN(e) ? E[e] : E.isOwnProperty(e) ? +e : undefined; }
})(E || (E = {}));

var iN = 0, iS1 = "b", iS2 = "2";

var s = E[iN];   // === "a"
var n1 = E.toNumber(iS1);   // === 1
var n2 = E.toNumber(iS1);   // === 2

var es = E[iN] ? iN : undefined;  // E[iN] returns a ?:string. --- result == 0
var en1 = E.toNumber(iS1);   // == 1
var en2 = E.toNumber(iS1);   // == 2
```

<br/>
##### (B) Example illustrating `const enum` and `enum` being merged into one single type:

The following TypeScript code:

``` ts
// TypeScript
enum E {a, b, c}

let e :E;
const n :number = 1;
const s :string ="c";

// constant assignments
e = E.a;
e = E[2];
e = E["a"];

// variable assignments
e = E[n];
e = E[E[n]];
e = E[s];
```

... should result in the following JavaScript compilation result:

``` js
// JavaScript
var E;
(function (E) {
    E[E["a"] = 0] = "a";
    E[E["b"] = 1] = "b";
    E[E["c"] = 2] = "c";
    E.toNumber = function (e)
                 { return IsNaN(e) ? E[e] : E.isOwnProperty(e) ? +e : undefined; }
})(E || (E = {}));

var e;
var n = 1;
var s = "c";

e = 0;
e = 2;
e = 0;

e = E[n] ? n : undefined;   // E[n] returns a ?:string. --- result == 1
e = E[n] ? n : undefined;   // The outer indexing operation is redundant and may be cancelled. --- result == 1
e = E.toNumber(s);     // == 2
```

<br/>
Some of the `E[n] ? n : undefined` constructs may be cancelled if runtime boundary checking isn't wanted/desired (new boolean compiler option?). So `e = E[n] ? n : undefined;` may then be transpiled to `e = n;`.
<br/>
In the discussion on Gitter I learned about #3507, #592. Yet I feel the above proposal will add value to the ongoing discussion on improving the `enum` types.