Propose simpler string constant

Ron Buckton Ron.Buckton at microsoft.com
Wed Dec 16 20:29:43 UTC 2015


In C#, an enum is a declaration that defines a distinct type consisting of a set of named constant values of that type. The distinct type of the enum has an underlying numeric base type, such as byte, int, long, etc. By default, each constant value is assigned an incrementing integer value starting from 0, although this sequence can be interrupted by assigning an explicit integer value:

```cs
enum Colors { Red, Green, Blue }
Colors.Red // 0
Colors.Green // 1
Colors.Blue // 2

enum Days: byte { Sat = 1, Sun, Mon, Tue, Wed, Thu, Fri }
Days.Sat // 1
Days.Sun // 2
```

C# enum values can be used with many standard numeric operations, and are often heavily used with bitwise operators. A C# enum value is a constant value, and is internally treated as a numeric literal by the compiler. In any C# enum declaration, only constant numeric values can be explicitly assigned to an enum member, although constant reduction is permitted:

```cs
enum UriComponents {
  Scheme = 0x1,
  UserInfo = 0x2,
  Host = 0x4,
  Port = 0x8,
  Path = 0x10,
  Query = 0x20,
  Fragment = 0x40,
  AbsoluteUri = Scheme | UserInfo | Host | Port | Path | Query | Fragment
}
```

Although C# enum values are converted to numeric literals by the compiler, their type information is preserved. This allows for an enum type to have different behavior from a literal numeric type. One example of this behavior is how ToString is handled on an enum value:

```cs
enum Numbers { Zero, One, Two }
(int)Numers.Zero // 0
Numbers.Zero.ToString() // Zero
```

The enum type itself has a number of static methods to make it easier to program against, including: GetName, GetNames, GetUnderlyingType, IsDefined, Parse, and ToObject. Instance members of an enum type include HasFlag and CompareTo.

In TypeScript we treat an enum declaration in a fashion similar to a C# enum, with respect to how we handle incremental integer values and explicitly assigned values. We effectively emit an enum as an object literal:

```ts
// TypeScript
enum Colors { Red, Green, Blue }

Colors.Red // 0

// JavaScript
var Colors;
(function (Colors) {
  Colors[Colors[0] = "Red"] = 0;
  Colors[Colors[1] = "Green"] = 1;
  Colors[Colors[2] = "Blue"] = 2;
})(Colors || (Colors = {}));

Colors.Red // 0
```

In this way, you can use `Colors.Red` to get the value 0, and `Colors[0]` to get the value "Red".  As a performance optimization we also have what we call "const enums". A const enum can be completely erased by the compiler:

```ts
// TypeScript
const enum Colors { Red, Green, Blue }

Colors.Red // 0

// JavaScript
0 /*Colors.Red*/ // 0
``` 

I think a general proposal for ES enums would be a combination of the above approaches, with some additions:

* An enum can be a declaration or an expression.
* The body of an enum consists of a new lexical scope.
* Enum members are standard JavaScript identifiers.
* Enum members are automatically assigned an incrementing integer value, starting at zero.
* Enum members can be explicitly assigned to an integer value, or another enum value.
* Within the body of an enum, Enum members can be accessed in the initializer without qualification.
* Within the body of an enum, Enum members are lexically declared names and cannot be accessed before they are defined (TDZ).
* An enum declaration can be called as a function to convert a string or numeric value into the enum value, making enum types distinct from numbers and from each other. [1]
* The result of `typeof` on an enum value is `enum`.
* Enum values support (at least) the following operators, returning an enum value of the same type: + (unary), - (unary), ~, + (binary), - (binary), | (binary), & (binary), ^ (binary).
* Any binary operation between two enums of different types is a TypeError. [1]
* Any binary operation between an enum and a number first converts the number to the enum type. If the number is not an integer it is a TypeError.
* Any binary operation between an enum and a string first converts the enum into the string value for that enum based on the enum member's JavaScript identifier (if present), or the string representation of its integer numeric value. [2]
* Calling Number() with an enum value as its first argument returns its underlying number value.
* Calling String() with an enum value as its first argument returns the string value for the enum member that defines the number (if present), or the string representation of its integer numeric value. [2]
* Calling the valueOf() instance method on an enum value has the same effect as Number() above.
* Calling the toString() instance method on an enum value has the same effect as String() above. [2]
* Two enum members on the same enum or differing enum types with the same underlying integer value are equivalent (==) but not strictly/reference equivalent (===). [1]

I think these rules could satisfy both anyone who needs enum values to be numeric (to support bitwise operations, bitmasks, ordinal indices, etc.) and those that would like enum values to be unique in a fashion similar to using Symbol().

[1] We have noticed in TypeScript some issues with Symbol-like equivalence with enums if you have two versions of the same module in NodeJS due to specific version dependencies, where you could have a.Color.Red !== b.Color.Red if *a* and *b* are different versions of the same module. Generally I think having enum values just really be numbers and not differing between == and === is less of a footgun.

[2] If enum values of different types should be === to each other, you should not be able to get a different result when you call .ToString(). In that case, we could add a static `getName` method to the enum type to get the string value for an enum.

Ron

> -----Original Message-----
> From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of 
> Brendan Eich
> Sent: Wednesday, December 16, 2015 10:04 AM
> To: Alican Çubukçuoğlu <alicancubukcuoglu at gmail.com>; es- 
> discuss at mozilla.org
> Subject: Re: Re: Propose simpler string constant
> 
> On Wed, Dec 16, 2015 at 9:41 AM Alican Çubukçuoğlu 
> <alicancubukcuoglu at gmail.com <mailto:alicancubukcuoglu at gmail.com> >
> wrote:
> 
> 
> 	How are enums declared?
> 	```javascript
> 	let myVar = 13;
> 	enum myEnum = {Red, Green, Blue};
> 
> 
> No `=` between name and `{`.
> 
> Enumerator scope is a good question. Clearly we do not want global scope.
> Rather, as a declaration immedicately contained by a block or top 
> level, we want lexical scope for the enum name -- and (I think) for the enumerators'
> individual names.
> 
> What about enumerator name scope for enum in class, without `static`?
> I'm not sure, but precedent says that the enumerator names define 
> prototype properties.
> 
> Expression as well as declaration `enum` form follows class and 
> function precedent. Expression form requires a reserved identifier 
> (not sym or sum or whatever), which `enum` has been forever, fortunately.
> 
> I agree symbol values by default, with ` = 0` or some other number 
> than
> 0 after the first enumerator name, looks confusing. Recall the first 
> use-case in the o.p. was (implicit, should rather be explicit) 
> reflection on the string value that spells the enumerator name.
> 
> /be


More information about the es-discuss mailing list