Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Merge enum and const enum features #6695

Open
SetTrend opened this issue Jan 28, 2016 · 23 comments
Open

Proposal: Merge enum and const enum features #6695

SetTrend opened this issue Jan 28, 2016 · 23 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@SetTrend
Copy link

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


#### So, given the above prerequisites, here are two examples, hopefully shedding some light upon my proposal:
##### (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:

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

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

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

The following TypeScript code:

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

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

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;`.
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.
@masaeedu
Copy link
Contributor

What happens if someone wants a toNumber entry in their enum?

@SetTrend
Copy link
Author

Good call.

The toNumber() member function I used just for visualization. It may alternatively be called __toNumber().

Or a Symbol may be used instead.

@RyanCavanaugh
Copy link
Member

I want to first go back to the original problems you outlined to make sure we would be solving them in the least disruptive way.

  1. enum values cannot be converted to their number equivalent, only to their string representation.

I don't understand this claim. Given this code:

enum E { a, b, c }
let x = E['a']; // x: E, x = 0
let s = Math.random() > 0.5 ? 'b' : '1';
let y = E[s]; // y: any, coerce to number if you're sure about s

So there's already a mechanism in place to convert from strings to numbers. It's an intentional safeguard that indexing by a string doesn't produce a number, because not every string index actually does produce a number.

  1. 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.)

This is a bit like phrasing "abstract classes can't be instantiated" as a problem -- one of the key features of const enums is that they don't expose their names or lookup values at runtime. If you want string <-> number lookups, don't use const. Is there a feature of non-const enums that you would like them to have instead?

  1. const enum values can not be converted to enum values and vice versa.

Can you clarify what this means? It's intentional that two arbitrary enums aren't considered compatible.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs More Info The issue still hasn't been fully clarified labels Jan 28, 2016
@SetTrend
Copy link
Author

Sure:

re. (1):

I don't understand this claim.


See this example:
enum E { a, b, c }
const a :number = E["a"];

results in this TSC error message:

error TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.

So one cannot actually use a string index value on enum types.

The reason for using TypeScript is using types. Forcing an enum into any to make the above statement work is in contrast to the reason for having TypeScript.


re. (2):

If you want string <-> number lookups, don't use const. Is there a feature of non-const enums that you would like them to have instead?


It sure is. But it's unnecessary to introduce a new type for them. The standard `enum` type can handle these cases as well (see my examples).

One cannot export or display const enum variables as strings. This lacking feature massively limits the use of them to a certain extent. Not being able to save const enum values in human readable format one cannot, for instance, create configuration files, like tsconfig.json from run-time values. One would be required to double-define, or mirror, each of the const enum values into enum types to be able to export them in human readable format.

See the TypeScript project, for example: There are loads of const enum values. None of them can be exported in human readable format or displayed in error messages.

The core issue is: Would it be preferable to change these const enum values all back to enum so they can be used in any situation? Or would it be preferable to have all const enum type defined and maintained twice (a twin sibling enum matching and mirroring each const enum)? Or would it rather be preferable to have enum values act like const enum values where appropriate?

I don't see a reason for enum values to keep the big footprint they currently have when assigning constant values to them.


re. (3):

const enum values can not be converted to enum values and vice versa.

Can you clarify what this means?


That's my conclusion from the user story I'm facing: being able to export the current TypeScript `const enum` values into a generated `tsconfig.json` file and to provide sensibel error messages.

From within the context of the program itself one might only need const enum values due to the speed they offer. But when these values need to leave the program realm it's impossible to create their string representation. That's what I meant with that statement. To be able to persist/output a const enum value one would need something similar to casting it to an enum value to be able to retrieve their string representation.

My statement was meant on a metaphysical level. I believe my proposal solves this challenge by having the advantages of both, const enum and enum, all in one.

@mykohsu
Copy link

mykohsu commented Jan 30, 2016

I actually came here to look for some answers regarding a different enum scenario, but the string to number conversion for enum actually works fine for me on 1.7.6

enum E { a, b, c }
const a :number = E["a"];

which generates

(function (E) {
    E[E["a"] = 0] = "a";
    E[E["b"] = 1] = "b";
    E[E["c"] = 2] = "c";
})(E || (E = {}));
var a = E["a"];

@SetTrend
Copy link
Author

@mykohsu : That's because in your TSC configuration (e.g. tsconfig.json file) you are implicitly allowing any type to be assumed <any>, i.e. treating your TypeScript code as intermediate JavaScript (i.e. typeless). The corresponding option is noImplicitAny, which defaults to false.

If you set TSC to strict mode (i.e. require strong typing) by setting the above option to true, you will see the error.

@mykohsu
Copy link

mykohsu commented Jan 30, 2016

C:\Temp>type test.ts
namespace Test {
  enum E { a, b, c }
  const a :number = E["a"];
}
C:\Temp>tsc --noImplicitAny test.ts

C:\Temp>type test.js
var Test;
(function (Test) {
    var E;
    (function (E) {
        E[E["a"] = 0] = "a";
        E[E["b"] = 1] = "b";
        E[E["c"] = 2] = "c";
    })(E || (E = {}));
    var a = E["a"];
})(Test || (Test = {}));

Still works

@SetTrend
Copy link
Author

@mykohsu : Good observation!

I can only suspect that this is a strange special treatment of constant index expressions.

While this indeed works:

enum E { a, b, c }
const a :number = E["a"];

... this doesn't:

enum E { a, b, c }
const s = "b";
const a :number = E[s];

@RyanCavanaugh
Copy link
Member

Indexing by a string literal produces the property with the same name.

When there's indirection involved, we can't detect this, but I think this will be changing at some point with string literal types when the expression is a string-initialized const (ping @DanielRosenwasser on this)

@DanielRosenwasser
Copy link
Member

That's something we're considering - #6080 is the closest thing that tracks it.

I don't think we should change the indexing behavior, but I do think the fact that you can't internally use a const enum and externally expose a runtime-dependent API is a problem, and it's a problem that we've run into in the compiler itself. The way we get around this is entirely a hack - we use --preserveConstEnums and strip the const modifiers out of the declaration file.

@mhegazy mhegazy added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed Needs More Info The issue still hasn't been fully clarified labels Feb 20, 2016
@SetTrend
Copy link
Author

Is there any progress on this?

@mhegazy
Copy link
Contributor

mhegazy commented Jul 19, 2016

it is marked as "Needs Proposal", so we are looking for a detailed proposal for the feature. Please see Writing Good Design Proposals.

@SetTrend
Copy link
Author

SetTrend commented Jul 19, 2016

😲 Isn't my proposal above, which is rather detailed, sufficient? It contains all the use cases there are.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 19, 2016

We are looking for a little bit more details. things you would think about if you were to sit down and implement it today.

@zuzusik
Copy link

zuzusik commented Nov 27, 2017

would be really great to have this feature someday

@SetTrend
Copy link
Author

Anyone want to continue this issue?

@SetTrend
Copy link
Author

In the eve of TypeScript 3.0, can anyone at the TypeScript team create a proposal for this?

@SetTrend
Copy link
Author

SetTrend commented Jan 7, 2019

ping

@SetTrend
Copy link
Author

SetTrend commented Jan 7, 2019

@mhegazy:

Do you know why this issue isn't listed when looking for issues? Currently no-one will find it and work on it.

@DanielRosenwasser
Copy link
Member

Hey @SetTrend please vote using 👍 or 👎 rather than pinging subscribers of the issue. The team prioritizes based on general demand, and proposals are welcomed by the community.

@SetTrend
Copy link
Author

SetTrend commented Jan 7, 2019

Hi @DanielRosenwasser , the issue with this issue is (or better: "was" as my ping seems to have fixed this now): This issue isn't (wasn't) found anymore (see my previous comment). Things cannot be voted upon if they are not found.

@DanielRosenwasser
Copy link
Member

Not sure what you're running into but it comes up when searching for "merge enum" https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+merge+enum

@SetTrend
Copy link
Author

SetTrend commented Jan 7, 2019

I don't wish to bloat this off-topic-issue, but this morning a query searching for issues raised by me returned the following result:

1

After my ping, the same query now returns the correct result:

2

But, please, let's not get off topic here dealing with some GitHub peculiarity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants