New-style Enums

Topics: General, Language Specification
Mar 12, 2013 at 4:57 PM
Edited Mar 12, 2013 at 4:59 PM
So it looks like the TS team is changing enums to be of the form:
enum Dog {
    Labrador: 1,
    Collie: 2,
    Poodle: 3
}
The current form uses equal signs (as in C#).

Since the design of enums is clearly open to change anyway, I'd like to see what other people think about the idea of going the whole 9 yards and making enums more similar to Java enums.

This basically would mean that you can have an enum of any type, not just integers, and that enums can contain methods of their own. In my opinion, this is a lot more powerful and useful than C#-style enums.

For example, the following is possible in Java with enums (shamelessly taken from Oracle's website):
public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
}
Despite the obvious syntax differences, I think some of the features in that example are useful examples of what an enum could/should be able to do.

For me, the most important feature is the ability to make enums of any type. Integers are not the only things that can be constant. (I am aware that in TS/JS it is possible to make a readonly object that behaves the same way as an enum, but this is a lot of extra code and it just isn't the same as first-class support).

Thoughts?

Related workitem
Mar 12, 2013 at 10:22 PM
I would rather them represent primitives instead of maintain state or have logic personally. They need to be able to appear in switch statements and be |'d together. The only thing TypeScript should do is basically make them a subtypes of number (or maybe string too) so you can restrict yourself to a specific enumerate at compile-time only.
Mar 20, 2013 at 9:14 PM
An enum should be usable as an array subscript and switch case which normally implies the equivalent of an integer. Your Planet enum would be readily implemented as a static array class member and doesn't result significantly more code (just a conventional enum and a get accessor):
class Planet
{
   static enum planets {Mercury, Venus};
   static planetData =
      [{mass:3.303e+23, radius: 2.4397e6},
       {mass:4.869e+24, radius: 6.0518e6}];
}
Mar 20, 2013 at 9:36 PM
@cretz I'm not sure where you got the impression they were maintaining state- they are still constant and immutable.

The ORing issue is a tricky one. I've seen others mention before that it probably was a mistake overloading enums in C# to be both a collection of constants AND a collection of flags. I think actually those two things make more sense as different structures.

@stanthomas Static classes members are not equivalent to enums.

function myFunc(foo: MyEnum) {
    
}

//Whoops, this won't work like you expect
function myFunc2(foo: MyEnum2) {

}

enum MyEnum {
    Uno: 1,
    Dos: 2
}

class MyEnum2 {
    static Uno = "One";
    static Dos = "Two";
}
See the problem?

Indexing into arrays with enums seems like an abuse to me. It's using what should be implementation details (the underyling type of a constant). Indexing into an object with them makes more sense, which would still be possible with my suggestion, you would just need to be cognizant of what the underlying type is (and whether it overloads toString()).

Switching on a more robust enum type could be possible as well since the compiler has all the type information it needs to rewrite such a switch statement into === statements.
Mar 21, 2013 at 3:44 PM
I got that impression by the way that Java enums can work (though it's basically static state since they are essentially multiple singletons of a type). This has already been solved in the 9.x branch (ref the lagnuage spec). Enums will not have methods, and they will be considered numbers by the type checking engine. Basically, they satisfy "number" and "number" satisfies the enum but enum types cannot satisfy one another. The runtime support for this is minimal and translates directly.

I like it though there are a few problems. I thin the main one I see is if you want to get all enums for a type, you can't do a for-in...This is because the keys of the enum object are both the ordinal values AND the enum names. Same problem with getting the count (oh only if we had macros or access to some of the compile-time information inline).
Mar 21, 2013 at 4:18 PM
Edited Mar 21, 2013 at 4:37 PM
But an enum is a type and the class Planet is a namespace, so you could write
function myFunc2 (p: Planet.planets)

-------------------

An enum is commonly understood to be a set of distinct values that can be assumed by a variable of that type. Conceptually, the underlying representation of the values is an internal implementation detail. The values form a sequence so one can be incremented to another and greater than another. Use to index arrays is natural and desirable. The underlying implementation as labels for a sequence of integer values is often exposed but not strictly necessary. (What you proposed is a collection, not an enum.)

You could see
enum color {red, blue, green};          // note no association with underlying value
as syntactic sugar for
#define red 0
#define blue 1
#define green 2
and I would be perfectly happy if TypeScript were to implement enums in this way, writing the underlying integer into the generated javascript.
But it would be nice if some additional compile time features and checks were added, e.g.
enum color {red, blue, green};

color.blue=color.green;               // error

var background: color = color.blue;   // ok
background = 5;                       // error
background = 0;                       // ideally an error too

var b: bool = ++color.red == color.blue;     // will be true
b = color.blue > color.red;                  // will be true
b = color.blue > 0;                          // ideally an error but could be
                                             // undefined behaviour
++color.green;                               // is undefined

var pallette: string[color] = [];        // a fixed size array accepting index values
                                         // from the color enum
pallette[color.red] = '#ff0000';         // ok
pallette[5] = 'five';                    // error
and the behaviour of javascript functions, toString() etc., is undefined but probably use the underlying integer
Coordinator
Mar 26, 2013 at 12:19 AM
@MgSam - Just a quick note that the syntax change you referenced in your original post was actually undone in the most recent spec updates. Enums will continue to use '=', to avoid any confusion with the intiailization semantics, which cannot be exactly the same as object literals due to the frequent need to reference previous field values in initializer expressions.

As for the other suggestions here, definitely interested to keep getting input on what would be useful.
Mar 26, 2013 at 2:54 PM
Edited Mar 26, 2013 at 2:55 PM
@stanthomas Your first example is just using enums within a module, I don't see how that encapsulates any of the behavior I've mentioned.

Your second example doesn't make sense to me. Why should red + 1 equal blue or blue be greater than red? If I wrote code like that I'd want the compiler to tell me I was doing something stupid and show an error. Enums are constants. The fact that they use integers should be an implementation detail, unless you explicitly type the members as numbers indicating each constant represents that number value. Making them sorta constants, sorta flags, sorta integers as C# does actually makes them more error prone and less useful.

It sounds like maybe what you really would like is #typedef, which in my opinion is a separate feature.

@LukeH Thanks for the update!
Mar 26, 2013 at 3:09 PM
Edited Mar 26, 2013 at 3:29 PM
Also thought I should mention what was brought up in the latest blog post- the overload on constants. This will be super useful to model a ton of existing JS libraries out there.

It also immediately reminds me of doing something something similar with hypothetical string enums but getting even better intellisense and strong typing (i.e., no need to remember the exact string your call requires).
interface Document {
    createElement(TagName.Canvas): HTMLCanvasElement;
    createElement(TagName.Div): HTMLDivElement;
    createElement(TagName.Span): HTMLSpanElement;
}

enum TagName{
    Canvas = "canvas",
    Div = "div",
    Span = "span"
}

var a = createElement(TagName.Canvas); //a is of type HTMLCanvasElement
Mar 26, 2013 at 9:45 PM
Edited Mar 26, 2013 at 9:48 PM
@MgSam:
  1. "function myFunc2 (p: Planet.planets)" addresses your earlier example of a form that wouldn't work by providing one that uses an enum defined as a static member of a class which you had said would not work.
  2. You will find that I did not write red+1 but ++red which is not the same thing.
  3. "Enums are constants. The fact that they use integers should be an implementation detail ...". Yes. I think you'll find that's what I said.
  4. What you want is not an enum but some sort of map of tuples.
Apr 2, 2013 at 7:47 AM
@MgSam:

I agree that enums should be any type (or at least also strings) and not just integers. As I see it the problem is that the underlying value doesn't stay internal as soon
as you transpile the code from TS to JS.

To mention a few upsides of using for example strings:

1) Many existing JS frameworks use a limited set of strings values that would be a perfect match for enums. I didn't see many
use cases for enums for existing JS frameworks when using an integer as underlying implementation.

2) When debugging and inspecting properties it is much easier to see a string value that makes some sense than an integer.

3) Automatic generated number are very fragile when it comes to serializing and deserializing objects over the time. For example when I store some object
as JSON that has enums and retrieve it at a later stage when in the mean time the code has an additional enum value, the
results could be wrong based on the position of the added enum value.
Jun 18, 2013 at 9:29 PM
Edited Jun 18, 2013 at 9:31 PM
So upon playing with the 0.9 release in the Playground I immediately noticed that enums now allowed arbitrary types in their assignment without complaint from the compiler. I assumed this was intended behavior. For example, putting strings in there works exactly as you would expect, except for the fact that the compiler complains about the cast at the very end:
enum Dog {
     Rover = 'Bark',
     Fido = 'Woof'
}

alert(<string>Dog.Rover); //The code works though, this outputs 'Bark'
I filed a bug about the cast error message and it was closed saying that enums still only support numbers. What is the reasoning behind this? I can understand arbitrary types introduce some additional complexity about declaration order but it seems bizarre that even strings are disallowed when they already work without issue.

The syntax I use above has two benefits over the Dog[Dog.Rover] syntax:
a) It's a lot clearer. The indexing syntax requires knowledge of the codegen for enums to understand what the heck it means. The new syntax would only really be useful in those situations where you want an enum whose values have both string and number representations.
b) It allows strings with spaces, which is far more useful.

It's a shame to see this feature taking shape with the exact same limitations as the crippled C# enum. I hope you guys revisit this issue or at least provide some clarification. Thanks.
Developer
Jun 18, 2013 at 11:54 PM
The Playground is actually not reporting the correct errors here. In Visual Studio you will see an error on the initializers with string values. I would suggest re-opening your issue with clarifications (since as noted in there, there weren't bugs in the feature per se) or logging a new one specifically for the suggestion of non-number values.