microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.09k stars 12.5k forks source link

Allow enums of types other than number #1206

Closed jhlange closed 7 years ago

jhlange commented 10 years ago

I'm reopening this issue, because it was closed with the move from codeplex, and doesn't seem to have been re-opened. https://typescript.codeplex.com/workitem/1217

I feel like this is very important for a scripting language.-- Especially for objects that are passed back and forth over web service calls.-- People generally don't use integer based enums in json objects that are sent over the wire, which limits the usefulness of the current enum implementation quite a bit.

I would like to re-propose the original contributor's suggestions verbatim.

jhlange commented 10 years ago

From the original proposal

As in the title, and as discussed extensively here, it would be very helpful to allow enums of types other than number. At the very least, if allowing arbitrary types is too much work, string enums should be allowed. The current codegen for enums actually works with strings as-is, the compiler just flags errors.

Consider:

enum Dog{ Rover = 'My Dog', Lassie = 'Your Dog' }

alert(Dog.Rover);

As of 0.9, this gets to compiled to:

var Dog; (function (Dog) { Dog[Dog["Rover"] = 'My Dog'] = "Rover";

Dog[Dog["Lassie"] = 'Your Dog'] = "Lassie"; })(Dog || (Dog = {}));

alert(Dog.Rover);

which is 100% functioning JavaScript that works as you expect it to.

In addition, the whole concept of "overloads on constants" would be a lot cleaner with a string-based enum:

interface Document { createElement(tagName: TagName): HTMLCanvasElement; }

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

var a = createElement(TagName.Canvas); //a is of type HTMLCanvasElement

Closed Jul 28 at 5:18 PM by jonturner

As part of our move to GitHub, we're closing our CodePlex suggestions and asking that people >move them to the GitHub issue tracker for further discussion. Some feature requests may already >be active on GitHub, so please make sure to look for an existing issue before filing a new one.

You can find our GitHub issue tracker here: https://github.com/microsoft/typeScript/issues

jhlange commented 10 years ago

Changed the one example from the original for the Document interface. It seems that you would only specify the name of the enum.-- Values passed in would become bounded to the use of the enum constants (and possibly to free-form string literals, where their values can be statically determined to be within the enum's domain values)

basarat commented 10 years ago

@jhlange I think tagged unions would automatically cater for this use case nicely : https://github.com/Microsoft/TypeScript/issues/1003

jhlange commented 10 years ago

@basarat That seems interesting from a theoretical standpoint, and I can definitely see uses for it.

It would solve my case.--At least giving some level of intellisense and compile-time validation.

I personally believe accessing enum constant fields makes for a much more natural experience for non-functional languages (and at least has parity with enums, which is what they they really are. I really don't think we should create a new concept for a construct that already exists. That will be confusing to too many people).

saschanaz commented 10 years ago

I would love this when I deal with C++ enums compiled by Emscripten.

// C++
enum Month {
  Jan, Feb, Mar
};
// I can do this, but they really are not numbers!
declare enum Month {
   Jan, Feb, Mar
}

interface EmscriptenEnum {
  value: number; /* some more properties ... */
}
interface Month extends EmscritenEnum {
}
declare module Month {
   var Jan: Month;
   var Feb: Month;
   var Mar: Month;
}
// Month.Jan.value == 0, Month.Feb.value == 1, ...
mwisnicki commented 9 years ago

C#/C++ lets you define base type of enum, although restricted to numeric type. I would like to be able to do the same but generalized to arbitrary type.

It should work with builtin types like string, so:

enum Foo extends string {
    BAR,
    BAZ = "surprise"
}

compiles to:

var Foo;
(function (Foo) {
    Foo["BAR"] = "BAR";
    Foo[Foo["BAZ"] = "surprise"] = "BAZ";
})(Foo || (Foo = {}));

but also user types, to handle enum object pattern that is used by enums in Java and common in other languages (as requested above):

interface IFoo {
    id: string;
    code: number;
}
enum Foo extends IFoo {
    BAR = { id: "BAR", code: 123 },
    BAZ = { id: "", code: 0 }
}

compiles to:

var Foo;
(function (Foo) {
    Foo["BAR"] = { id: "BAR", code: 123 };
    Foo["BAZ"] = { id: "", code: 0 };
})(Foo || (Foo = {}));

Perhaps with a convention that if interface has fields id and/or code (or ordinal as in Java) they can be omitted in initializer and filled by compiler.

enoshixi commented 9 years ago

How would this work?

enum Test {
    Foo = "Bar",
    Bar = "Baz",
    Baz = "Foo"
}
mwisnicki commented 9 years ago

Either compilation error or omit generation of reverse mapping for Bar. I'm not sure which one I prefer.

mwisnicki commented 9 years ago

In fact I'd prefer if TypeScript didn't generate reverse mapping in the same object. Perhaps all reverse mappings (for number based enums too) should go inside some property, say Test.__reversed.

jhlange commented 9 years ago

In my mind these really need to compile down to plain strings. String enums are already heavily used in json objects for Web services. In my opinion this is one of the main use cases for string based enums.-- another example is in the second post-- being able to define the domain values for predefined Javascript apis.

If it isn't possible to define strongly typed interfaces for these cases, because typescript uses a different methodology in its implementation, the implementation would be purely academic. On Dec 30, 2014 3:16 PM, "Marcin Wisnicki" notifications@github.com wrote:

In fact I'd prefer if TypeScript didn't generate reverse mapping in the same object. Perhaps all reverse mappings (for number based enums too) should go inside some property, say Test.__reversed.

— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-68409570 .

NN--- commented 9 years ago

I think the first thing should be strings const enum. This is definitely the most useful feature. Many APIs has JSON with small amount of possible string values and currently you must use 'string' type rather then a correct subset. In that case I even think the names are not needed since you can pass only the correct string.

const enum A {
 "x",
 "y",
 "z"
}

var a : A;
a = // Intellisense suggest "x", "y" or "z"
a = "x"; // OK
a = "b"l // Error
jhlange commented 9 years ago

My concern with the const enum approach is that it will lead to cases where the value can not be determined to be conforming when being passed into an API. (would this be a warning?, what happens if you get that warning? do you have to add casts all over the place to mitigate the warning?).

If you treat them as something that can never be converted to or from a string, they will do exactly what they need to do.-- It should work exactly like the integer enums, with the exception that the values are strings. I think it would be fine if the keys and values are locked together, meaning that

const enum A {
 "x",
 "y",
 "z"
}

Could be fine, but any string assignments without casts should fail.

public myfunc(someRandomInput: string) {
var a : A;
a = // Intellisense suggest A.x   A.y   A.z
a = "x"; // Error, it is overkill to support this special case. Just use A.x like a regular enum
a = A.x; // OK
a = "b"; // Error
a = someRandomInput; // Error, can not always be determined to be conforming.
}

In cases where they are converted from a string, a cast should be used.-- But even then, behavior like that can indicate that an input did not come from a proper source.

Additionally, this will help tooling like the the schema validation/generators to appropriately generate the appropriate validation code.-- Schema validation is going to become even more important in times to come.--One of my big concerns with javascript is general is the lack of validation.-- Now that people are running servers on this stuff.

As far as I can tell, the bulk of the work needed to get this done is removing one validation/compiler error against enum definitions (assigning types other than number).-- There might be some smarts to make sure people don't directly assign numbers to enum, but they shouldn't be doing that without a cast anyway either...

teppeis commented 9 years ago

How about using generics for enum? Current existing enum means enum<number>. So we can extend it to enum<string> or other types:

enum<string> Foo1 {
    BAR, // default value is "BAR".
    BAZ = "x" // also you can specify the vaule.
}
var e: Foo1 = Foo1.BAR; // ok
var s: string = Foo1.BAR; // ok
var n: number = Foo1.BAR; // error

enum<boolean> Foo2 {
    BAR = true, // assigning is required. only number and string enum have default value.
    BAZ = false
}

You can use every type for enum like enum<YourFavoriteType> and there is no breaking change.

mwisnicki commented 9 years ago

Base type syntax makes more sense IMHO and is already familiar to C# developers.

bvaughn commented 9 years ago

+1 for the generics suggestion.

teppeis commented 9 years ago

and Closure Compiler uses @enum {string}, similar to generics.

/**
 * @enum {string}
 */
var Foo = {
  BAR: 'BAR',
  BAZ: 'BAZ'
};

Also default type of enum in Closure is number. It's in common with TypeScript's case.

If the type of an enum is omitted, number is assumed. https://developers.google.com/closure/compiler/docs/js-for-compiler

dead-claudia commented 9 years ago

+1 for the generics syntax. I just need some sort of functionality. Here's my use case: I'm wanting to convert this bit of ES6 + Lodash into something I can more conveniently statically check. (Note: the boilerplate is because I use it for other things as well, things that would have to be generated at build time to statically type-check.)

// Enum
const tokenTypes = makeEnumType(([value, length]) => ({value, length}));

export const Tokens = tokenTypes({
    OpenCurved:   ['(', 1],
    CloseCurved:  [')', 1],
    OpenBracket:  ['[', 1],
    CloseBracket: [']', 1],
    OpenCurly:    ['{', 1],
    CloseCurly:   ['}', 1],
    Identifier:   ['Identifier', 0],
    String:       ['String', 0],
    EOF:          ['EOF', 0],
});

// Utilities
function deepFreeze(obj) {
    Object.freeze(obj);
    _.forOwn(obj, value => typeof value === 'object' && deepFreeze(obj));
    return obj;
}

_.mixin({
    removeProto(obj) {
        let ret = Object.create(null);
        _.forOwn(obj, _.partial(_.assign, ret));
        return ret;
    },
    freeze: deepFreeze,
});

function mapObject(obj, f) {
    let ret = Object.create(Object.getPrototypeOf(obj));
    forOwn(obj, (value, i) => ret[i] = f.call(obj, value, i, obj));
    return ret;
}

function makeEnumType(transformer) {
    return obj => _.chain(mapObject(obj, transformer))
        .removeProto()
        .freeze()
        .value();
}

The best I can currently do in the first case is this (the second is similar):

interface TokenType {
    type: string;
    value: string;
}

function type(value: string, length: number): TokenType {
    return {value, length};
}

class TokenTypes {
    // This shouldn't be called as a constructor...but it still type-checks.
    static OpenCurved: TokenType   = type('(', 1);
    static CloseCurved: TokenType  = type(')', 1);
    static OpenBracket: TokenType  = type('[', 1);
    static CloseBracket: TokenType = type(']', 1);
    static OpenCurly: TokenType    = type('{', 1);
    static CloseCurly: TokenType   = type('}', 1);
    static Identifier: TokenType   = type('Identifier', 0);
    static String: TokenType       = type('String', 0);
    static EOF: TokenType          = type('EOF', 0);
}

With the above syntax, I could use the following:

enum<TokenType> TokenTypes {
    OpenCurved   = type('(', 1),
    CloseCurved  = type(')', 1),
    OpenBracket  = type('[', 1),
    CloseBracket = type(']', 1),
    OpenCurly    = type('{', 1),
    CloseCurly   = type('}', 1),
    Identifier   = type('Identifier', 0),
    String       = type('String', 0),
    EOF          = type('EOF', 0),
}

This is where enums can really help.

(The boilerplate is mainly for places where a preprocessor would be helpful.)

jhlange commented 9 years ago

@jbondc I like it.

You've picked the simple, obvious and intuitive solution, that can enforce type safety in many of the situations that some of the above options can't without a full static analysis of a program.-- It additionally would support creating straight forward domain bound json schma validations right from the typescript definition.

Any interest in #2491 ?

mohsen1 commented 9 years ago

+1 for this and I am suggesting supporting all primitive types for enum values. Just like Swift. This is the syntax in Swift which I really like:

enum Audience: String {
    case Public = "Public"
    case Friends = "Friends"
    case Private = "Private"
}
dead-claudia commented 9 years ago

I would like to mention that, because of ECMAScript language limitations themselves, const enums should be limited to non-Symbol primitives. Because {} !== {} and Symbol("foo") !== Symbol("foo"), other enums can't be inlined.

I do feel this would be incredibly useful, though.

On Tue, Jun 16, 2015, 16:41 Mohsen Azimi notifications@github.com wrote:

+1 for this and I am suggesting supporting all primitive types for enum values. Just like Swift. This is the syntax in Swift which I really like:

enum Audience: String { case Public = "Public" case Friends = "Friends" case Private = "Private" }

— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-112563174 .

Gambero81 commented 9 years ago

+1 for the generics syntax. +1 for jbondc proposal for primitive type / non primitive type management It would be useful emit primitive enum value for primitive type enums:

enum myStringEnum < string > {
   one = "myOneValue",
   two = "myTwoValue",
   three = "myThreeValue"
}

var value = myStringEnum.one; //emits 'var value = "myOneValue" /* myStringValue.one */'

emit inline value for primitive types (number, string, boolean ...) is very useful because the enum declaration does not need to be emitted in javascript but is used only to enforce compile-type validation

danquirk commented 9 years ago

@Gambero81 are you aware of const enums for the purposing of skipping emit like you want?

Gambero81 commented 9 years ago

@danquirk const enum are great, but attually does not support generics type but is only for numeric type..

dead-claudia commented 9 years ago

Also const enums should be restricted to primitive types, and the whole reason they aren't emitted is to lessen code size (especially minified), which would not usually be the case with other types, such as strings and booleans.

But as for normal enums of non-numeric types, this definitely should be possible. Currently, there is little reason to prefer a normal enum over a const enum, and this would wonderfully fix that.

I do have (another) possible syntax, in case you all might like it:

function type(ch: string, length: num): TokenType {
    // code...
}

enum TokenTypes: TokenType {
    OpenCurved   = type('(', 1),
    CloseCurved  = type(')', 1),
    OpenBracket  = type('[', 1),
    CloseBracket = type(']', 1),
    OpenCurly    = type('{', 1),
    CloseCurly   = type('}', 1),
    Identifier   = type('Identifier', 0),
    String       = type('String', 0),
    EOF          = type('EOF', 0),
}
bertvanbrakel commented 9 years ago

Coming from the Java world I really miss the ability to define methods on my enums. Would really like for enums to be full class citizens but still able to be used in switch statements (with auto complete support)

Allows for things like:

var planet = Planet.fromOr(myForm.planetSelect.selectedIndex,Planet.Earth)
myForm.swallowEnabled.checked=Planet.Earth.canSwallow(planet);

Example enum:

enum Planet {
     private label:string; <--custom property
     private size:string; <--custom property
     private orbit:number; <--custom property

     Mercury("Mercury",1,1),Venus("Venus",2.8,2),Earth("Home,3,3)...; <--declare 'constants'

     Planet(label,size,orbit){ <--private constructor
       .....
     }

     //a custom instance method
     public canSwallow(planet:Planet):bool { //<--custom method
        return planet.size < this.size;
     }

     public isCloserToSun(planet:Planet):bool { //<--custom method
        return planet.orbit < this.orbit;
     }

     //all functions below auto generated, or implemented in base enum. Shown here in semi typescript

     //convert from string, number or type or use given default
     public static fromOr(nameindexOrType:string,defVal:Planet=null):Planet { //<--auto generated
        var e = from(nameindexOrType);
        return e==null?defVal:e;
     }

     //convert from string, number or type or return null
     public static from(nameindexOrType:string):Planet { //<--auto generated
       if(nameindexOrType == null){ return null; }
       if(typeof(nameindexOrType) =='Number'){
         switch(nameindexOrType){
           case 0:return Planet.Mercury;
           case 1:return Planet.Venus;
           ...
        }
      }if(typeof(nameindexOrType) =='String'){
         nameindexOrType = nameindexOrType.ToUpperCase();
         switch(nameindexOrType){
           case 'MECURY':return Planet.Mercury;
           ...
        }
      }
      return null;
    }

    public static get names():string[] { //<--auto generated
       return ['Mercury','Venus','Earth',...];
    }

    public static get values():Planet[] { //<--auto generated
       return [Planet.Mercury,Planet.Venus,Planet.Earth',...];
    }

  }
}

internally there would also be a field called 'index' and 'name' which are used for comparison checks (or just one of them)

If no custom properties or methods, then everything compiled down to a number or a string only.

dead-claudia commented 9 years ago

@bertvanbrakel I like your idea, but I think that may fit better as another bug, and also, this may fall under "Not yet". Another thing, IMO, there aren't a lot of use cases for having methods on enums, anyways, since they can easily become too coupled to the enum, and hard to generalize.

dead-claudia commented 9 years ago

So far, here's the possible syntaxes I've found here...

// 1.
enum Enum extends string {
    Foo, // "Foo"
    Bar = "something",
}

// 2.
enum<string> Enum {
    Foo, // "Foo"
    Bar = "something",
}

// 3.
enum Enum: string {
    Foo, // "Foo"
    Bar = "something",
}

WDYT?

RyanCavanaugh commented 9 years ago

@bertvanbrakel You can already do this:

enum Color { Red, Green, Blue }
module Color {
  export function getFavorite() { return Color.Green; }
}
mwisnicki commented 9 years ago

He wants methods on enum values (objects), similar to Java enum.

Gambero81 commented 9 years ago

there are many good proposal, but the question is: when will be sheduled and implemented? there is a roadmap for this task?

dead-claudia commented 9 years ago

@Gambero81 The "needs-proposal" tag is that this needs a more formal proposal before it actually gets implemented, including type-checking semantics, required JS emit, etc.

dsebastien commented 9 years ago

+1 for more versatile enums. Also +1 @bertvanbrakel's proposal. Having enums with multiple properties, private constructors and methods all together in a single/self-contained unit of code is very useful and safe from a developer's pov and useful for testability. In addition to defining methods, being able to implement interfaces is also quite useful.

Here's an example of a useful Java enum; in it the 'value' property allows to make the enum usage independent of the enum ordinal values; this allows to avoid common issues with enum values reordering and the like that often bite beginners.

public enum DeploymentStatus implements InternationalizedEnum {
    PENDING(100, "STATUS_PENDING", "status.pending"),
    QUEUED_FOR_RELEASE(110, "STATUS_QUEUED_FOR_RELEASE","status.queuedrelease"),
    READY_FOR_RELEASE(120,"STATUS_QUEUED_RELEASE","status.readyrelease"),
    RELEASING(130,"STATUS_RELEASING","status.startedrelease"),
    RELEASED(140,"STATUS_RELEASED","status.suceededrelease"),
    QUEUED(200, "STATUS_QUEUED", "status.queued"),
    READY(300, "STATUS_READY", "status.ready"),
    STARTED(400, "STATUS_STARTED", "status.started"),
    SUCCEEDED(500, "STATUS_SUCCEEDED", "status.succeeded"),
    FAILED(600, "STATUS_FAILED", "status.failed"),
    UNKNOWN(700, "STATUS_UNKNOWN", "status.unknown"),
    UNDEFINED(0, "", "status.undefined");

    private final int value;
    private final String code;
    private final String messageCode;

    private DeploymentStatus(final int value,
                             final String code,
                             final String messageCode) {
        this.value = value;
        this.code = code;
        this.messageCode = messageCode;
    }

    public String getCode() {
        return code;
    }

    public int getValue() {
        return value;
    }

    public String getMessageCode() {
        return messageCode;
    }

    public static DeploymentStatus parse(final Integer id) {
        DeploymentStatus status = DeploymentStatus.UNDEFINED;
        if (id != null) {
            for (DeploymentStatus entry : DeploymentStatus.values()) {
                if (entry.getValue() == id) {
                    status = entry;
                    break;
                }
            }
        }
        return status;
    }

    public boolean isFinalStatus() {
        return this == SUCCEEDED || this == FAILED || this == UNKNOWN;
    }
}
dead-claudia commented 9 years ago

@dsebastien +1 That's almost the use case I needed.

I did find that for my particular use case, it just needed refactored.

dead-claudia commented 9 years ago

@jbondc

The emit is the same but the compiler should treat the entire object as an immutable/const structure.

What about method calls? You have to partially evaluate the code to figure that out.

dsebastien commented 9 years ago

I stick with my idea that the enums should remain self-contained as that's the cleanest; having to create a separate class just for the purpose of 'linking' it from an enum doesn't make much sense, assuming that the concept it describes is an element of the enum and the operations that it supports. Indeed it would make it much closer to a class but with hard restrictions :)

Also, i don't know if having a mutable enum entry is a good idea at all, it's the kind of thing that would get abused and would lead to side-effects. For me, enums should be as immutable as can be ^^

jhlange commented 9 years ago

It would be great if this feature accurately reflects how the APIs people use today are structured (see the original few comments). The whole purpose of typescript is to introduce a basic level of type safety, not implement features from functional languages, etc. The use cases that already exist to support this have already been defined. Some of these include:

  1. By the browser standards (example: document.createElement(elementType: HtmlElementType) )
  2. By well established javascript libraries ( element.on(eventType: EventType) ) https://api.jquery.com/on/
  3. Basically every web api on the internet ( units of measure, etc in example https://developer.yahoo.com/weather/ )
  4. By the return value of JSON.parse() and stringify() (just look at your own projects, around serialization of types)
  5. by popular server side libraries http://expressjs.com/4x/api.html#app.onmount
  6. Probably lots more that I'm not thinking about

If enums become complex types, tagged unions, or any of the other more complex suggestions in this thread, they will be relatively useless for type safety (the primary goal of typescript), as they will not be usable in scenarios surrounding any current APIs (or in the case of tagged unions, you'd still be compatible, but would the type safety can't be inferred without a full static analysis of the project, which typescript does not do).

Many of the comments here have a lot of academic merit, but they simply do not reflect interoperability with any of the built in APIs, and therefore will deliver minimal value.

dead-claudia commented 9 years ago

_Edit 1: add type inference based on first entry_ _Edit 2: It's now a gist_

I'm going to take a stab at a more concrete proposal, but first, I'd like to say the following:

  1. Much of the reason people are pushing for enums of type other than number are for string enums (we all know they are necessary) and type-safe enums. Also, occasionally, object enums come in handy, so why not support that.
  2. I don't like the current syntax floating around, because it's too similar to interface key type declarations, instead of a variable type or enum property. Also, it doesn't make a whole lot of sense to me in theory - the semantics of current enums are closer to that of the following:

    enum Foo {
    }
  3. It doesn't seem to fit with the syntax of the rest of the enum body, where names and values are separated with an equals operator.
  4. What I'm about to propose would keep the interior as a simple list, simply expanding the inference a bit. That would also simplify the necessary changes to the parser, becoming more purely additive.
  5. This is still backwards-compatible with the original semantics, including with const enums. The explicit version's type is enforced during creation unlike the implicitly numeric version. Example below (the second would fail to compile):

    enum Foo {
     Bar = 'Bar', // not a string, but checks anyways
    }
    
    enum Foo: number {
     Bar = 'Bar', // Error: not a string
    }

A little comparison:

// Current format floating around:
enum TokenTypes {
    [prop: string]: TokenType;

    OpenCurved   = type('(', 1),
    CloseCurved  = type(')', 1),
    OpenBracket  = type('[', 1),
    CloseBracket = type(']', 1),
    OpenCurly    = type('{', 1),
    CloseCurly   = type('}', 1),
    Identifier   = type('Identifier', 0),
    String       = type('String', 0),
    EOF          = type('EOF', 0),
}

// What I'm about to propose:
enum TokenTypes: TokenType {
    OpenCurved   = type('(', 1),
    CloseCurved  = type(')', 1),
    OpenBracket  = type('[', 1),
    CloseBracket = type(']', 1),
    OpenCurly    = type('{', 1),
    CloseCurly   = type('}', 1),
    Identifier   = type('Identifier', 0),
    String       = type('String', 0),
    EOF          = type('EOF', 0),
}

(And yes, my proposal will allow for any arbitrary type for the enum properties. That's intentional - no need to guess the enum type)

Proposal for typed enums

These are all amendments to the enum part of the spec.

Enum Declaration

The enum syntax would be extended as follows, where EnumName is the enum's name, Type is the enum's type, and EnumMembers are the members and associated values of the enum type.

  EnumDeclaration:    enumEnumName{EnumBodyopt}    enumEnumName:Type{EnumBodyopt}

  EnumName:    Identifier

An enum type of the form above is a subtype of Type, and implicitly declares a variable of the same name, with its type being an anonymous object containing all the type's names as keys and Type as the value type of each of them. Moreover, if Type is the Number or Symbol primitive types, then the object's signature includes a numeric index signature with the signature [x: number]: string or [x: symbol]: string, respectively. In the first variation, Type is inferred to be the type of the EnumValue of the first EnumEntry of EnumBody if it has an EnumValue (i.e. it's initialized), or the Number primitive type otherwise. In the second variation, Type is explicitly given.

  EnumDeclaration:    constEnumBodyoptenumEnumBodyoptEnumNameEnumBodyopt{EnumBodyopt}    constEnumBodyoptenumEnumBodyoptEnumNameEnumBodyopt:EnumBodyoptPrimitiveEnumTypeEnumBodyopt{EnumBodyopt}

  PrimitiveEnumType:    boolean    string    number    void

An enum type of the form above is a subtype of PrimitiveEnumType. It declares a variable of the same name, with its type being an anonymous object containing all the type's names as keys and PrimitiveEnumType as the value type of each of them. It is said to also be a constant enum type. In the first variation, Type is inferred to be the type of the EnumValue of the first EnumEntry of EnumBody if it has an EnumValue (i.e. it's initialized), or the Number primitive type otherwise. In the second variation, Type is explicitly given. For constant enum types, for the sake of simplicity below, Type references PrimitiveEnumType.

The example

enum Color: string { Red, Green, Blue }

declares a subtype of the String primitive type called Color, and introduces a variable 'Color' with a type that corresponds to the declaration

var Color: {
    [x: string]: string;  
    Red: Color;  
    Green: Color;  
    Blue: Color;  
};

The example

enum Color: Type { Red, Green, Blue }

declares a subtype of the type Type called Color, and introduces a variable 'Color' with a type that corresponds to the declaration

var Color: {
    Red: Color;  
    Green: Color;  
    Blue: Color;  
};

Enum Members

Each enum member has an associated value of Type specified by the enum declaration.

  EnumBody:    EnumMemberList,opt

  EnumMemberList:    EnumMember
   EnumMemberList,EnumMember

  EnumMember:    PropertyName
   PropertyName=EnumValue

  EnumValue:    AssignmentExpression

If in an ambient context:

If Type is explicitly given, an error occurs if a given EnumValue of any EnumMember of EnumMemberList is not of type Type.

For each EnumMember, if EnumValue is not given, then EnumValue is defined in the first of the following to apply:

  1. If Type is the String primitive type, then let EnumValue be PropertyName.
  2. Else, if Type is the Number primitive type, then:
    1. If the member is the first in the declaration, then let EnumValue be the primitive number 0.
    2. Else, if the previous member's EnumValue can be classified as a constant numeric enum member, then let EnumValue be the EnumValue of the previous member plus one.
    3. Else, an error occurs.
  3. Else, if Type is the Symbol primitive type, then let EnumValue be PropertyName.
  4. Else, if the constructor for Type accepts a single parameter of the String primitive type, then let EnumValue be a newly constructed instance of Type with a sole argument PropertyName.

    Non-normative:

    In other words, Type's constructor must implement

    interface TypeConstructorWithString {
       new (value: string): <Type>;
    }
  5. Else, if the constructor for Type accepts a single parameter of the Number primitive type, then:

    1. If the member is the first in the declaration, then let EnumValue be let EnumValue be a newly constructed instance of Type with a sole argument of the primitive number 0.
    2. Else, if the previous member's EnumValue is a newly constructed instance of Type, and the constructor is called with a sole argument that can be classified as a constant numeric enum member, then let EnumValue be that constructor call's argument plus one.
    3. Else, an error occurs.

    Non-normative:

    In other words, Type's constructor must implement

    interface TypeConstructorWithNumber {
       new (value: number): <Type>;
    }
  6. Else, if the constructor for Type accepts zero parameters, then let EnumValue be a newly constructed instance of Type, with zero arguments.

    Non-normative:

    In other words, Type's constructor must implement

    interface TypeConstructorWithString {
       new (): <Type>;
    }
  7. Else, an error occurs.

A few examples:

EnumValue for Foo is the number 0.

enum E {
    Foo = 0,
}

EnumValue for Foo is the string "Foo".

enum E: string {
    Foo = "Foo",
}

EnumValue for Foo is a symbol wrapping the string "Foo".

enum E: symbol {
    Foo = Symbol("Foo"),
}

EnumValue for Foo is the number 0.

enum E {
    Foo,
}

EnumValue for Foo is the string "Foo".

enum E: string {
    Foo,
}

EnumValue for Foo is new Type("Foo").

class Type {
    constructor(public value: string);
}

enum E: Type {
    Foo,
}

EnumValue for Foo is new Type().

class Type {
    constructor();
}

enum E: Type {
    Foo,
}

An enum member is classified as follows:

A _constant enum expression_ is a constant boolean enum expression, a constant numeric enum expression, a constant void enum expression, or a constant string enum expression.

A _constant boolean enum expression_ is a subset of the expression grammar that can be evaluated fully at compile time, and returns only booleans. A constant boolean enum expression is one of the following:

A _constant numeric enum expression_ is a subset of the expression grammar that can be evaluated fully at compile time, and returns only numbers. An expression is considered a constant numeric enum expression if it is one of the following:

A _constant string enum expression_ is a subset of the expression grammar that can be evaluated fully at compile time, and returns only strings. An expression is considered a constant numeric enum expression if it is one of the following:

A _constant void enum expression_ is a subset of the expression grammar that consists of either the primitive null or the primitive undefined.

Proposed emit

Section 9.1 is amended to the following:

var <EnumName>;
(function (<EnumName>) {
    <EnumMemberAssignments>  
})(<EnumName>||(<EnumName>={}));

where EnumName is the name of the enum, and EnumMemberAssignments is a sequence of assignments, one for each enum member, in order they are declared. EnumMemberAssignments is defined in the first applicable section below.

  1. If Type is the Number or Symbol primitive types, let the following be EnumMemberAssignments.

    <EnumName>[<EnumName>["<PropertyName>"] = <EnumValue>] = "<PropertyName>";
  2. If Type is of any other type, let the following be EnumMemberAssignments.

    <EnumName>["<PropertyName>"] = <Value>;

Examples

For either of these sources,

enum Color { Red, Green, Blue }
enum Color: number { Red, Green, Blue }

the following should be emitted:

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

For this source,

enum Color: string { Red, Green, Blue }

the following should be emitted:

var Color;
(function (Color) {
    Color["Red"] = "Red";
    Color["Green"] = "Green";
    Color["Blue"] = "Blue";
})(Color||(Color={}));

For this source,

enum Color: symbol { Red, Green, Blue }

the following should be emitted:

var Color;
(function (Color) {
    Color[Color["Red"] = Symbol("Red")] = "Red";
    Color[Color["Green"] = Symbol("Green")] = "Green";
    Color[Color["Blue"] = Symbol("Blue")] = "Blue";
})(Color||(Color={}));

For this source,

class Type {
    constructor(public value: String);
}

enum Color {
  Red = new Type("Red"),
  Green,
  Blue,
}
enum Color: Type { Red, Green, Blue }

the following should be emitted for either version of the Color enum:

var Color;
(function (Color) {
    Color["Red"] = new Type("Red");
    Color["Green"] = new Type("Green");
    Color["Blue"] = new Type("Blue");
})(Color||(Color={}));

For this source,

class Type {
    constructor();
}

enum Color: Type { Red, Green, Blue }

the following should be emitted for the Color enum:

var Color;
(function (Color) {
    Color["Red"] = new Type();
    Color["Green"] = new Type();
    Color["Blue"] = new Type();
})(Color||(Color={}));

For this source,

class Type {
    constructor(public value: number);
}

enum Color: Type { Red, Green, Blue }

the following should be emitted for the Color enum:

var Color;
(function (Color) {
    Color["Red"] = new Type(0);
    Color["Green"] = new Type(1);
    Color["Blue"] = new Type(2);
})(Color||(Color={}));

For this source,

class Type {
    constructor(public value: number);
}

enum Color: Type { Red = new Type(1), Green, Blue }

the following should be emitted for the Color enum:

var Color;
(function (Color) {
    Color["Red"] = new Type(1);
    Color["Green"] = new Type(2);
    Color["Blue"] = new Type(3);
})(Color||(Color={}));

For this source,

class Type {
    constructor(public value: string, public index: number);
}

enum Color: Type {
    Red = new Type("Red", 1),
    Green = new Type("Green", 2),
    Blue = new Type("Blue", 3),
}

the following should be emitted for the Color enum:

var Color;
(function (Color) {
    Color["Red"] = new Type("Red", 1);
    Color["Green"] = new Type("Green", 2);
    Color["Blue"] = new Type("Blue", 3);
})(Color||(Color={}));

For this source,

class Type {
    constructor(public value: string, public index: number);
}

enum Color: string {
    Red = "Red",
    Green = "Blue",
    Blue = "Purple",
}

the following should be emitted for the Color enum:

var Color;
(function (Color) {
    Color["Red"] = "Red";
    Color["Green"] = "Blue";
    Color["Blue"] = "Purple";
})(Color||(Color={}));

Some of the above examples translated below:

interface EmscriptenEnumEntry {
  value: number; /* some more properties ... */
}
declare enum Month: EmscriptenEnumEntry {Jan, Feb, Mar}
// Month.Jan.value == 0, Month.Feb.value == 1, ...

interface IFoo {
    id: string;
    code: number;
}
enum Foo: IFoo {
    BAR = { id: "BAR", code: 123 },
    BAZ = { id: "", code: 0 }
}

enum Audience: String {
    Public,
    Friends,
    Private,
}

enum Test: string {
    Foo = "Bar",
    Bar = "Baz",
    Baz = "Foo"
}

// const string enums now exist.
const enum A: string {x, y, z}

enum Foo2: boolean {
    BAR = true, // assigning required
    BAZ = false
}

enum TokenTypes: TokenType {
    OpenCurved   = type('(', 1),
    CloseCurved  = type(')', 1),
    OpenBracket  = type('[', 1),
    CloseBracket = type(']', 1),
    OpenCurly    = type('{', 1),
    CloseCurly   = type('}', 1),
    Identifier   = type('Identifier', 0),
    String       = type('String', 0),
    EOF          = type('EOF', 0),
}

enum tuples: [number, number] {
  a = [0, 1],
  b = [0, 2],
  // tuples...
}

/*
 * The Planets Java example
 */
class PlanetType {
    constructor(
        private label: string,
        private size: string,
        private orbit: number);

    canSwallow(planet: Planet): bool {
       return planet.size < this.size;
    }

    isCloserToSun(planet: Planet): bool {
       return planet.orbit < this.orbit;
    }

    static fromOr(nameindexOrType: string, defVal: Planet = null): Planet {
       var e = from(nameindexOrType);
       return e == null ? defVal : e;
    }

    static from(nameindexOrType: string): Planet {
        if (nameindexOrType == null) {
            return null;
        } else if (typeof nameindexOrType === 'Number') {
            switch(nameindexOrType){
            case 0: return Planet.Mercury;
            case 1: return Planet.Venus;
            case 2: return Planet.Earth;
            // ...
            }
        } else if (typeof nameindexOrType === 'String') {
            nameindexOrType = nameindexOrType.ToUpperCase();
            switch(nameindexOrType){
            case 'MERCURY': return Planet.Mercury;
            case 'VENUS': return Planet.Venus;
            case 'EARTH': return Planet.Earth;
            // ...
            }
        } else {
            return null;
        }
    }
}

enum Planet: PlanetType {
    Mercury = new PlanetType("Mercury", 1, 1),
    Venus = new PlanetType("Venus", 2.8, 2),
    Earth = new PlanetType("Home", 3, 3),
    // ...
}

/*
 * The DeploymentStatus Java example
 */
class DeploymentStatusType implements InternationalizedEnumType {
    constructor(
        private value: number,
        private code: string,
        private messageCode: string);

    getCode() { return code; }

    getValue() { return value; }

    getMessageCode() { return messageCode; }

    static parse(id: number) {
        if (id != null) {
            switch (id) {
            case 100: return DeploymentStatus.PENDING;
            case 110: return DeploymentStatus.QUEUED_FOR_RELEASE;
            case 120: return DeploymentStatus.READY_FOR_RELEASE;
            case 130: return DeploymentStatus.RELEASING;
            case 140: return DeploymentStatus.RELEASED;
            case 200: return DeploymentStatus.QUEUED;
            case 300: return DeploymentStatus.READY;
            case 400: return DeploymentStatus.STARTED;
            case 500: return DeploymentStatus.SUCCEEDED;
            case 600: return DeploymentStatus.FAILED;
            case 700: return DeploymentStatus.UNKNOWN;
            }
        }
        return DeploymentStatus.UNDEFINED;
    }

    isFinalStatus() {
        return this === DeploymentStatus.SUCCEEDED ||
            this === DeploymentStatus.FAILED ||
            this === DeploymentStatus.UNKNOWN;
    }
}

export default enum DeploymentStatus {
    PENDING = new DeploymentStatusType(100, "STATUS_PENDING", "status.pending"),
    QUEUED_FOR_RELEASE = new DeploymentStatusType(110, "STATUS_QUEUED_FOR_RELEASE", "status.queuedrelease"),
    READY_FOR_RELEASE = new DeploymentStatusType(120, "STATUS_QUEUED_RELEASE","status.readyrelease"),
    RELEASING = new DeploymentStatusType(130, "STATUS_RELEASING", "status.startedrelease"),
    RELEASED = new DeploymentStatusType(140, "STATUS_RELEASED", "status.suceededrelease"),
    QUEUED = new DeploymentStatusType(200, "STATUS_QUEUED", "status.queued"),
    READY = new DeploymentStatusType(300, "STATUS_READY", "status.ready"),
    STARTED = new DeploymentStatusType(400, "STATUS_STARTED", "status.started"),
    SUCCEEDED = new DeploymentStatusType(500, "STATUS_SUCCEEDED", "status.succeeded"),
    FAILED = new DeploymentStatusType(600, "STATUS_FAILED", "status.failed"),
    UNKNOWN = new DeploymentStatusType(700, "STATUS_UNKNOWN", "status.unknown"),
    UNDEFINED = new DeploymentStatusType(0, "", "status.undefined");
}

What do you all think about this?

dead-claudia commented 9 years ago

@jbondc

You're welcome. As for your nits:

a. The reverse mapping can still exist. Symbols are allowed to be object keys in ES6, and they are fully distinct. You are correct in that Symbol("Foo") !== Symbol("Foo"), but symbols are still reflexive. The following is still the case:

Enum.Foo = Symbol("Foo")
Enum.Foo !== Symbol("Foo")
Enum.Foo === Enum.Foo

Enum[Enum.Foo] = "Foo"

let sym = Enum.Foo
Enum[sym] === "Foo"

That's just ES6 semantics.

Strings aren't included because of their high risk of collision. Symbols are less likely to collide than even numbers.

b. I'd rather be covering obscure edge cases and be complete than to limit to seemingly sane options and be inconsistent. Who knows, maybe someone actually has some sort of very weird use case for a void enum, filled with only nulls and undefineds. I don't believe it's the language's job to make that judgement, but rather the programmer.

c. If you don't want value inference, then don't use it. Nothing is making you use it, and I know it wouldn't be that hard to write a tslint rule for that. Some people would love that ability to infer values (I know I would).

A lot of the extras are mostly in the realm of flexibility. The compiler's job isn't to enforce one specific code style, IMHO. Even Python's language design avoids that for the most part (main exception: multiline lambdas by Guido's decree).

dead-claudia commented 9 years ago

@jbondc

The reverse mapping is to be consistent with the current state, where there exists a reverse mapping. I'm okay with leaving Symbols out, though, and just sticking wiht numbers.

And there doesn't exist any inference for primitives except for strings, symbols, and numbers - explicit initialization is required for all values for any other primitive type, including booleans. Your third example would not work - it would fail to compile.

I didn't realize you wanted value inference as well, though. Sorry for that incorrect assumption.

As for type inference, I'll add the language in my proposal above.

dead-claudia commented 9 years ago

And as for the type inference part (after the edit), it doesn't care about the value, and string value inference is only based on the key, not previous values [1]. So, you would get the following, potentially surprising behavior in this edge case:

enum Enum: string {
  Foo = "some random string",
  Bar, // Bar
}

[1] Inferring a convention from an arbitrary string is theoretically impossible, and getting a computer to get even remotely close to a reasonable convention is still extremely hard to accomplish for most cases. It would make for a good AI research topic, but even then, it can get ambiguous without enough context. And I highly doubt the TypeScript compiler is going to get true AI any time in the next few years, if even within the next 20.

enum Error {
  UNKNOWN = "Unknown",

  // is this "Bad_Response", "Bad Response", "Bad response", "BadResponse",
  // or something else?
  BAD_RESPONSE,
}
dead-claudia commented 9 years ago

@jbondc (a) is already in my spec draft here.

dead-claudia commented 9 years ago

And for everyone here, I moved my proposal to a gist.

dead-claudia commented 9 years ago

@jbondc Would you mind punting the idea about extra string interpolation stuff until the rest of this gets solidified and merged? Then a followup bug would be a great place to bikeshed the rest of this out.

dead-claudia commented 9 years ago

I feel the rest of my proposal (see this gist) hasn't gotten the proper bikeshedding it needs.

/cc @danquirk @jhlange @basarat @SaschaNaz @NN--- @mwisnicki @enoshixi @teppeis @bvaughn

dead-claudia commented 9 years ago

Bump...could I get some people to look over my enum proposal?

ngbrown commented 9 years ago

My interest in this Enum issue was I could receive text in json and have it converted to the Enum type (with it's underlying number or string or whatever)

enum TestEnum {
    Unknown = 0,
    Alpha = 1,
    Beta = 2
}

interface IJsonTest {
    fieldOne: number;
    fieldTwo: TestEnum;
}

The goal is to receive and send this with a IJsonTest typed object:

{
  "fieldOne": 5,
  "fieldTwo": "Alpha"
}

It now seems that even the enum with a string type doesn't help with that situation. Without runtime metadata on the interface; how the json parser could even handle this?

dead-claudia commented 9 years ago

Under my proposal, your enum should be this:

enum TestEnum: string {
  Unknown, Alpha, Beta,
}

And as for inferring value types, it should work with similar machinery as the current numeric enums. If this works, then my proposal should suffice:

enum Foo { Bar, Baz }
let baz: Foo = 1
let bar: Foo = 0

In terms of validating input at compile time, there is no language or library in the world that can validate JSON input purely at compile time. That idea itself would be bad in practice because you can't assume your incoming JSON is exactly what you are expecting, especially on the server. Most validation has to happen at runtime.

On Wed, Jul 29, 2015, 18:47 Nathan Brown notifications@github.com wrote:

My interest in this Enum issue was I could receive text in json and have it converted to the Enum type (with it's underlying number or string or whatever)

enum TestEnum { Unknown = 0, Alpha = 1, Beta = 2 } interface IJsonTest { fieldOne: number; fieldTwo: TestEnum; }

The goal is to receive and send this with a IJsonTest typed object:

{ "fieldOne": 5, "fieldTwo": "Alpha" }

It now seems that even the enum with a string type doesn't help with that situation. Without runtime metadata on the interface; how the json parser could even handle this?

— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-126119055 .

DanielRosenwasser commented 9 years ago

@impinball sorry for the delay, things have been fairly busy. I haven't gotten the chance to read the whole proposal, but I do question the utility of a constant void enum so far.

dead-claudia commented 9 years ago

I am willing to remove it. It was mostly for completeness, but I'm not really that strongly attached to it.

On Thu, Jul 30, 2015, 13:48 Daniel Rosenwasser notifications@github.com wrote:

@impinball https://github.com/impinball sorry for the delay, things have been fairly busy. I haven't gotten the chance to read the whole proposal, but I do question the utility of a constant void enum.

— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-126415506 .

Roaders commented 9 years ago

Hi, I am not sure if I can vote for this or what but I'd just just to say that a String based Enum would be incredibly helpful for me. I have a problem that all Enums in our json are in string format. The server guys really don't want to convert them to integers so I have to consider changing the value of each one as the data arrives over the wire.