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

Allow enums of types other than number #1206

Closed
jhlange opened this issue Nov 19, 2014 · 123 comments
Closed

Allow enums of types other than number #1206

jhlange opened this issue Nov 19, 2014 · 123 comments
Assignees
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@jhlange
Copy link

jhlange commented Nov 19, 2014

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.

@danquirk danquirk added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Nov 19, 2014
@jhlange
Copy link
Author

jhlange commented Nov 19, 2014

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
Copy link
Author

jhlange commented Nov 19, 2014

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
Copy link
Contributor

basarat commented Nov 19, 2014

@jhlange I think tagged unions would automatically cater for this use case nicely : #1003

@jhlange
Copy link
Author

jhlange commented Nov 19, 2014

@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
Copy link
Contributor

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
Copy link

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
Copy link

How would this work?

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

@mwisnicki
Copy link

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

@mwisnicki
Copy link

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
Copy link
Author

jhlange commented Dec 30, 2014

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" [email protected] 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
#1206 (comment)
.

@NN---
Copy link

NN--- commented Jan 28, 2015

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
Copy link
Author

jhlange commented Feb 20, 2015

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
Copy link

teppeis commented Feb 25, 2015

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
Copy link

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

@bvaughn
Copy link

bvaughn commented Mar 11, 2015

+1 for the generics suggestion.

@teppeis
Copy link

teppeis commented Mar 22, 2015

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
Copy link

+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
Copy link
Author

jhlange commented May 30, 2015

@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
Copy link
Contributor

mohsen1 commented Jun 16, 2015

+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
Copy link

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 [email protected] 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
#1206 (comment)
.

@Gambero81
Copy link

+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
Copy link
Member

danquirk commented Jul 6, 2015

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

@Gambero81
Copy link

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

@dead-claudia
Copy link

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
Copy link

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
Copy link

@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
Copy link

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
Copy link
Member

Some discussion today. Unorganized notes; refer to #1206 (comment) for a mini-spec with some changes as follows

  • We don't want some alternate syntax at the declaration site - it's already in the "Oops we added top-level expression code" area and we want to keep that surface area absolutely minimum
  • Generating the reverse map would be a terrible idea
    • Higher chance of conflict between keys and values
    • You'd certainly want to be able to write a "Is this a valid key?" or "Is this a valid value?" function for a string enum; the reverse map would necessarily be a commingling of both
  • Because of transpile, we can't detect string vs non-string enum by looking at the initializers
    • This means all values would need to be exactly string literals
    • Note: No substitution literals! Remember we need to produce a concrete type out of this

@nevir
Copy link

nevir commented Apr 4, 2017

  • Because of transpile, we can't detect string vs non-string enum by looking at the initializers
    • This means all values would need to be exactly string literals

Would this restrict enums to just primitives (or even just number/string) - or would it be more widely applicable to any value (that exactly matches the enum's type)?

@RyanCavanaugh
Copy link
Member

I don't think enums with values other than strings or numbers are on the table at this point. It's unclear what a const enum with a reference type value would mean, and existing solutions (see #1206 (comment)) seem to be doing pretty well. And we're definitely not adding boolean enums 😉

@nevir
Copy link

nevir commented Apr 4, 2017

Makes sense

And we're definitely not adding boolean enums 😉

Pff, I swear there are totally legit reasons for

enum bool: boolean = {
  true = false,
  false = true,
};

@Buslowicz
Copy link

Actually you can achieve that with

enum bool {
  true = 0,
  false = 1
}

@nevir
Copy link

nevir commented Apr 4, 2017 via email

@dead-claudia
Copy link

@RyanCavanaugh Is it possible to allow reference type enums provided they aren't const enums? I don't see why every enum type has to have a const enum variant.

@ghost
Copy link

ghost commented Apr 5, 2017

@nevir Don't forget FileNotFound.

@RyanCavanaugh
Copy link
Member

@isiahmeadows it's possible, but it'd have to be well-justified because it's a lot more complexity. For a string enum we can just produce a union type out of the literal types of its values, but there's no corresponding behavior for reference types because there's no such thing as a literal type for a reference type value.

@dead-claudia
Copy link

@RyanCavanaugh Oh, I see now, and it's not really a short term need for me.

Maybe, in the future, could nominal subtyping could help?

@jquintozamora
Copy link

In my scenario I needed sort of custom object enum, since my class does not have any method that would make the inheritance necessary, I just used this class with static props:

export class ViewerItemCardType {
    public static Big: ViewerItemCardType = new ViewerItemCardType(1, "FeaturedBig", 330, 660);
    public static Medium: ViewerItemCardType = new ViewerItemCardType(2, "FeaturedSmall", 155, 310);
    public static Small: ViewerItemCardType = new ViewerItemCardType(3, "NormalArticle", 100, 200);
    private constructor(
        public id: number,
        public name: string,
        public imageHeight: number,
        public imageWidth: number
    ) { };
}

I can access to these "complex" enums like:

ViewerItemCardType.Big.imageHeight
ViewerItemCardType.Big
ViewerItemCardType.Small

@isiahmeadows , Does that particular scenario match with your definition at some point ?

@mindplay-dk
Copy link

@jquintozamora that's awesome!

I think you can infer the extra type-hints though, and you'd likely want to define a means of enumerating the options as well, depending on your use-case - so like:

export class ViewerItemCardType {
    public static Big = new ViewerItemCardType(1, "FeaturedBig", 330, 660);
    public static Medium = new ViewerItemCardType(2, "FeaturedSmall", 155, 310);
    public static Small = new ViewerItemCardType(3, "NormalArticle", 100, 200);
    public static All: ViewerItemCardType[] = [
        ViewerItemCardType.Big,
        ViewerItemCardType.Medium,
        ViewerItemCardType.Small
    ]
    private constructor(
        public id: number,
        public name: string,
        public imageHeight: number,
        public imageWidth: number
    ) { };
}

@mindplay-dk
Copy link

@jquintozamora also note that it's a closed set though - you can't use declaration merging to add new values, so that's another thing we'd (hopefully) get from real typed enums.

@jquintozamora
Copy link

Hi @mindplay-dk ,
In my current scenario is really helpful when used in combitation with react - style attribute.
Indeed, it would be good to have a official solution for that like real typed complex enums. :)

@ahejlsberg
Copy link
Member

Implementation now available in #15486.

@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels May 17, 2017
@mhegazy mhegazy added the Fixed A PR has been merged for this issue label May 17, 2017
@mhegazy mhegazy added this to the TypeScript 2.4 milestone May 17, 2017
@LMFinney
Copy link

LMFinney commented Jul 31, 2017

I released ts-enums as a library that enables creating full-class, Java-style enums. Maybe it can be useful for some people on this thread.

Suggestions for improvements are welcome :)

@shafeeqthayyil
Copy link

With Angular 2
//enum

export enum IType
{
Vegitable=0,
Fruits=1,
Fish=2
}

// angular 2 Component in type script

import {IType} from '/itype';
export class DataComponent
{
getType(id:number):any
{
      return IType[id];
}
}

// in your html file

<div>
{{getType(1)}}
</div>

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests