microsoft / TypeScript

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

Allow enums of types other than number #1206

Closed jhlange closed 7 years ago

jhlange commented 9 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.

ghost commented 9 years ago

@impinball :+1: +9001 I love this proposal.

I seldom use the enum type because it is only numeric, and it almost never makes sense to pass numeric values to/from e.g. JSON, web services, etc. This (along with #1003) would make enums actually practical to use, and remove the need for separate adapter/serializer classes.

irakliy81 commented 9 years ago

+1

PanayotCankov commented 9 years ago

:+1:

dead-claudia commented 9 years ago

I forgot about that part. Will fix as soon as I get to a computer.

On Sun, Sep 20, 2015, 09:26 Jon notifications@github.com wrote:

@impinball https://github.com/impinball I'd suggest moving the proposal into a repo. Remove the 'void' part as its not reasonable to implement since 'undefined' is used to know if a value can be resolved statically. Haven't looked at implementing non-primitives types so no comments there yet other then 'seems doable'.

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

dead-claudia commented 9 years ago

@jbondc Done and done. I was considering it for a bit, anyways. I've just been busy these past few weeks (school, trying to get a new web dev startup off the ground, etc.).

jbondc commented 9 years ago

Forked :wink: I'll send a pull request as I get around to it.

RicoSuter commented 8 years ago

Just FYI: With the current compiler (test it in the playground on typescriptlang.org), you can write this:

export enum Language {
    English = <any>"English",
    German = <any>"German",
    French = <any>"French",
    Italian = <any>"Italian"
}

Which generates:

(function (Language) {
    Language[Language["English"] = "English"] = "English";
    Language[Language["German"] = "German"] = "German";
    Language[Language["French"] = "French"] = "French";
    Language[Language["Italian"] = "Italian"] = "Italian";
})(exports.Language || (exports.Language = {}));
var Language = exports.Language;

Isn't this what we need?

RicoSuter commented 8 years ago

In VS, IntelliSense works for me... and I do not need to assign string to lang; you can assign using lang = Language.French... But of course, a compiler feature would be better... however, it solves the problem for me..

basarat commented 8 years ago

The workaround I've been using : https://basarat.gitbooks.io/typescript/content/docs/tips/stringEnums.html :rose:

Gambero81 commented 8 years ago

@rsuter you found a great workaround for string enum, unfortunally it doesn't work with const enum, my primary usage of string enum would be for inline const values without need of transpile code..

dead-claudia commented 8 years ago

Ping on my proposal.. I wish I could get a little more critiquing on it.

DanielRosenwasser commented 8 years ago

@impinball, we discussed the matter at our last design meeting (#5740). We're trying to come up with a future-proof idea where enums might individually have their own types as well, and so it's not clear exactly what the most ideal way to do that is while allowing values other than primitive literals.

dead-claudia commented 8 years ago

@DanielRosenwasser How does that factor into my proposal? I'm willing to withdraw it if it's not very good.

DanielRosenwasser commented 8 years ago

It isn't bad at all, but if we take enums in a direction where each enum member claims a type representing its value (e.g. a string enum creating a string literal type alias for each member), it isn't clear what kind of literal type we would create for each member of a non-primitive enum. That would be a weird inconsistency between a string enum and a Dog enum.

dead-claudia commented 8 years ago

Okay. I'm guessing you're talking in the same vein of string literal types, like type Foo = "foo" | "bar"?

On Mon, Nov 23, 2015, 23:17 Daniel Rosenwasser notifications@github.com wrote:

It isn't bad at all, but if we take enums in a direction where each enum member claims a type representing its value (e.g. a string enum creating a string literal type alias for each member), it isn't clear what kind of literal type we would create for each member of a non-primitive enum. That would be a weird inconsistency between a string enum and a Dog enum.

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

mhegazy commented 8 years ago

It is more like trying make string literal types and enums make sense togather.

michaelmesser commented 8 years ago

Any chance this will be implemented?

DanielRosenwasser commented 8 years ago

Just as an update, we're still actively discussing this, but the upcoming features in 2.0 have had priority, so no solid plans yet.

danielepolencic commented 8 years ago

Just a note on this.

Sometimes it's very useful to have methods attached to the enum. As an example take a look at this groovy code that does a custom reverse lookup:

public enum ColorEnum {
    WHITE('white', 'White is mix of all colors'),
    BLACK('black', 'Black is no colors'),
    RED('red', 'Red is the color of blood')

    final String id;
    final String desc;
    static final Map map 

    static {
        map = [:] as TreeMap
        values().each{ color -> 
            println "id: " + color.id + ", desc:" + color.desc
            map.put(color.id, color)
        }

    }

    private ColorEnum(String id, String desc) {
        this.id = id;
        this.desc = desc;
    }

    static getColorEnum( id ) {
        map[id]
    }
}

@rauschma has another good example using his library enumify:

    class Weekday extends Enum {
        isBusinessDay() {
            switch (this) {
                case Weekday.SATURDAY:
                case Weekday.SUNDAY:
                    return false;
                default:
                    return true;
            }
        }
    }
    Weekday.initEnum([
        'MONDAY', 'TUESDAY', 'WEDNESDAY',
        'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY']);

    console.log(Weekday.SATURDAY.isBusinessDay()); // false
    console.log(Weekday.MONDAY.isBusinessDay()); // true

Is there any chance we could have something similar for Typescript as well?

EDIT: Just noticed that the two examples are somehow misleading. I think the current proposal will allow us to have methods attached to the constant:

class TokenType {
    constructor(public kind: string, public length: number) {}

   myMethod() {
      return;
   }
}

enum TokenTypes: TokenType {
    OpenCurved = new TokenType('(', 1)
}

TokenTypes.OpenCurved.myMethod();

what I'm suggesting is methods attached to the enum:

class TokenType {
    constructor(public kind: string, public length: number) {}
}

enum TokenTypes: TokenType {
    OpenCurved = new TokenType('(', 1)

    fromString(string: tokenKind) {
       return reverseLookupTable.find(token => token.kind === tokenKind);
   }
}

token = TokenTypes.fromString('(');
token === TokenTypes.OpenCurved
basarat commented 8 years ago

@danielepolencic you can add static methods (documented here as well https://basarat.gitbooks.io/typescript/content/docs/enums.html) :

enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}
namespace Weekday {
    export function isBusinessDay(day: Weekday) {
        switch (day) {
            case Weekday.Saturday:
            case Weekday.Sunday:
                return false;
            default:
                return true;
        }
    }
}

const mon = Weekday.Monday;
const sun = Weekday.Sunday;
console.log(Weekday.isBusinessDay(mon)); // true
console.log(Weekday.isBusinessDay(sun)); // false

:rose:

danielepolencic commented 8 years ago

Thank you for that. I never thought about it 🙇

I think the only outstanding item would be to found a way to do the reverse lookup.

I'll provide an example so that I can shed some light on how I use enums.

Imagine you have a <select> with 3 <option>s: High, Low and Medium. Since those are constants, I'd code them as an enum:

enum Frequency {High, Medium, Low}

At this point, I wish to render the <select> in the page. Unfortunately, I can't enumerate the constants in the enum so I end up doing:

<select>
  <option value="{{Frequency.High}}">{{Frequency.High}}</option>
  <option value="{{Frequency.Low}}">{{Frequency.Low}}</option>
  <option value="{{Frequency.Medium}}">{{Frequency.Medium}}</option>
</select>

Time to write some tests. I'd like to grab the value from the current selection, transform it into an enum and compare it. Ideally I'd write something like this:

// dispatch click and select Low
const currentValue: string = document.querySelector('select').value;
const frequency: Frequency = Frequency.fromString(currentValue);
expect(frequency).toEqual(Frequency.Low);

With the current enum implementation, I can't reverse lookup the string value.

The other issue is connected to the value in the <option> tag. I wish I could use an identifier rather than the value of the constant. E.g.:

<select>
  <option value="{{Frequency.High.id}}">{{Frequency.High.name}}</option>
  <option value="{{Frequency.Low.id}}">{{Frequency.Low.name}}</option>
  <option value="{{Frequency.Medium.id}}">{{Frequency.Medium.name}}</option>
</select>

where the constant is a tuple [name, id]. Since now I can retrieve a constant by name and id, I'd also like to write a new method that does that for me:

enum Frequency {
  fromName(name: string): Frequency {}
  fromId(id: number): Frequency {}
}

Again, I think this is not doable in the current proposal.

What I'm proposing is an enum implementation more similar to Java/Groovy enums. That would solve the issues above.

dead-claudia commented 8 years ago

@danielepolencic BTW, current enums are technically number subtypes, and I don't see anything innately helpful in making Java-like enums, which are instanceof the enum type itself. Also, it doesn't solve the problem of primitive string enums, which would be helpful for, say, diagnostic messages.

saschanaz commented 8 years ago

@danielepolencic You can do reverse lookup as Frequency["High"] and Frequency[0] both works. Enumeration also works:

enum Frequency { High, Medium, Low }

for (const key in Frequency) if (isNaN(+key)) console.log(key);
// High, Medium, Low
basarat commented 8 years ago

@danielepolencic as mentioned reverse lookup is supported out of the box :

enum Tristate {
    False,
    True,
    Unknown
}
console.log(Tristate[0]); // "False"
console.log(Tristate[Tristate.False]); // "False" because `Tristate.False == 0`

More : https://basarat.gitbooks.io/typescript/content/docs/enums.html#enums-and-strings :rose:

amcdnl commented 8 years ago

+1 for string enums.

I'd expect the following ( which actually work if you ignore the errors ;) )

export enum SortDirection {
  asc = 'asc',
  desc = 'desc'
}

I don't know any JS dev who ever wants numbers in this scenario.

ghost commented 8 years ago

I'd like it if the type of SortDirection was a string literal type ('asc'|'desc') instead of just string; or at least if 'asc'|'desc' was implicitly coercable to SortDirection.

mindplay-dk commented 8 years ago

I really was expecting the following to work:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

enum Direction {
    Sideways,
    InCircles
}

In my opinion, having enums represented by numbers was a mistake - it doesn't work well with declaration merging. Strings would have had a much lower chance of colliding - they would also have been easier to debug, and they are much more commonly used in everyday JS.

Anyways, for backwards compatibility, you could preserve the current enum behavior (enum would be inferred as enum<number> for BC) and simply add generic enum types, e.g.:

enum<string> Direction {
    Up,
    Down,
    Left,
    Right
}

enum<string> Direction {
    Sideways,
    InCircles
}

These would be much simpler to merge and even check the merge at compile-time to make sure the same name was only defined once. They would auto-initialize as strings, much the same way enums work for numbers now, and of course you'd be allowed to initialize them with a string expression yourself.

If somebody wants an enum<Foo> or even enum<any> for some reason, more power to them - an enum is simply a set of named values. It doesn't need to be more than that, because we still have classes:

class Color {
    constructor(red, green, blue) { ... }

    static Red = new Color(255, 0, 0)
    static Green = new Color(0, 255, 0)
    static Blue = new Color(0, 0, 255)
}

That works just as well - in fact, that's what I'm doing now, since numbered enums don't work well for me, and this works better in terms of declaration merging as well.

I don't know, I don't think we need to come up with something massively complicated for this - we still have classes covering a lot of these requirements, just open up enums to other types besides numbers and I'd be happy :-)

frogcjn commented 8 years ago

Is there any process?

danielepolencic commented 8 years ago

@basarat I was just playing with the reverse lookup and still can get my head around this:


enum Level {Medium, Low, High}

function printVolume(level: Level): void {
  console.log(`volume is ${level}`);
}

const currentVolume = 'Medium';

printVolume(Level[currentVolume]);

throws an error complaining that Element implicitly has an 'any' type because index expression is not of type 'number'.

I tried to cast it to Level, didn't work either.

image

(Yes, I have "noImplicitAny": true in my tsconfig.json)

DanielRosenwasser commented 8 years ago

@danielepolencic that has more to do with allowing using string literal types for element accesses: #6080

Taytay commented 8 years ago

I like the workarounds proposed here, but I couldn't get them to easily work with function overloading the way I wanted them to, so I came up with this. It's verbose, and possibly more complicated than it needs to be, but it gives the compiler enough information that it lets me express things the way I expected to be able to:

Here's the playground link, and here is the code:


type MyEnum = "numberType" | "stringType" | "objectType";

// The advantage of declaring this class is that we could add extra methods to it if we wanted to.
class MyEnumClass {
    NumberType: "numberType" = "numberType";
    StringType: "stringType" = "stringType";
    ObjectType: "objectType" = "objectType";

    // You could declare methods here to enumerate the keys in the enum, do lookups, etc.
}

const MyEnum = new MyEnumClass();

// You can use it like a normal enum: 
let x : string = MyEnum.NumberType;
console.log(x); // prints "numberType" 

// But you can also use it for operator overloading:

function someFunc(x : "numberType") : number;
function someFunc(x : "stringType") : string;
// This declaration is equivalent to the above, and personally I find it more readable
function someFunc(x : typeof MyEnum.StringType) : string;
function someFunc(x : "objectType") : Object;

function someFunc(x : MyEnum) : number | string | Object;
function someFunc(x : MyEnum) : number | string | Object
{
    switch(x){
        case MyEnum.NumberType:
            return 5;
        case MyEnum.StringType:
            return "a string";
        case MyEnum.ObjectType:
            return {foo : "bar"};
    }
}

let someNumber1 : number = someFunc(MyEnum.NumberType)
let someString : string = someFunc(MyEnum.StringType);
let someObject : Object = someFunc(MyEnum.ObjectType);

// And this errors with "Type 'string' is not assignable to type 'number'" just as we would expect:
let someNumber2 : number = someFunc(MyEnum.StringType);
RyanCavanaugh commented 8 years ago

Things have shifted a bit with the introduction of string literal types. Picking up from @isiahmeadows 's proposal above https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-121926668 , considering a restricted "string-only" enum that would behave as follows

// Please bikeshed syntax!
enum S: string {
  A,
  B = "X",
  C
}

would be exactly equivalent to this code:

namespace S {
  export const A: "A" = "A";
  export type A = "A";
  export const B: "X" = "X";
  export type B = "X";
  export const C: "C" = "C";
  export type C = "C";

  // Imagine this were possible
  [s: string]: S;
}
type S = S.A | S.B | S.C;

the equivalent code starting with const enum S: string { would be identical in the type system, but not have any emit. I'll sketch up an implementation in the next few weeks and we'll see how it looks.

mariusschulz commented 8 years ago

To clarify:

[…] the equivalent code starting with const enum S: string { would be identical in the type system, but not have any emit.

Does that mean that the following code …

const enum S: string {
  A,
  B = "X",
  C
};

var strings = [S.A, S.B, S.C];

… would be transpiled with inlined string values, like this?

var strings = ["A", "X", "C"];
RyanCavanaugh commented 8 years ago

Correct

mariusschulz commented 8 years ago

Great, that would be useful in many situations. Very much looking forward to further work on this one!

dead-claudia commented 8 years ago

I could see that as potentially useful myself. I've been personally toying with the idea of rewriting the DOM API definitions from the ground up with the latest TS, and that would also help clean up many of the occasionally magical string and boolean parameters used all over the place in it so they're more descriptive.

(IIRC const enums work as expected in definition files, correct?)

On Fri, Aug 19, 2016, 12:28 Marius Schulz notifications@github.com wrote:

Great, that would be useful in many situations. Very much looking forward to further work on this one!

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-241065785, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBLcSvWekopof3YsYZCtBP6vGiz7Nks5qhdmQgaJpZM4C9e4r .

ghost commented 8 years ago

@RyanCavanaugh I'm hoping Object.keys, Object.values, and Object.entries behave sanely for your revised enums (they should, as long as nothing extra gets tacked on). The current implementation of enums has properties for both keys and values (for reverse-lookup), so you get a mixture of both when iterating.

dead-claudia commented 8 years ago

@errorx666 I'd expect they would, unless any of the strings clash.

dead-claudia commented 8 years ago

@RyanCavanaugh Could symbols be included in that addition? That'd be fairly useful as well. (They can't be in const enums, but they would make for safer JS interop.)

nevir commented 8 years ago

Should string enums still be generated with the reverse mapping? (value to key)

In some cases it's useful to be able to determine whether a value exists in the enum at runtime. You can currently do that by only looking at numeric values. If both keys and values are strings, I'm not sure how to accomplish that

RyanCavanaugh commented 8 years ago

I'd say that's a strong argument for not producing the reverse map. Object.keys / Object.values would then produce a clean set of key names / values. Going from value -> key would be slightly awkward but I can't imagine why you'd need to do that.

nevir commented 8 years ago

Yeah, turning the reverse map off would be fantastic - or at least behind a flag

On Tue, Aug 30, 2016, 17:31 Ryan Cavanaugh notifications@github.com wrote:

I'd say that's a strong argument for not producing the reverse map. Object.keys / Object.values would then produce a clean set of key names / values. Going from value -> key would be slightly awkward but I can't imagine why you'd need to do that.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-243623868, or mute the thread https://github.com/notifications/unsubscribe-auth/AAChnUl8PHgQ69whvb2xbzvHMkdzqOj2ks5qlMtngaJpZM4C9e4r .

dead-claudia commented 8 years ago

I agree in the reverse mapping not existing for strings for that very reason. I don't usually need them anyways (I usually create the reverse mapping manually in JS, and I've rarely needed it in practice outside of pretty printing, which is also rare because of the type system).

On Tue, Aug 30, 2016, 20:39 Ian MacLeod notifications@github.com wrote:

Yeah, turning the reverse map off would be fantastic - or at least behind a flag

On Tue, Aug 30, 2016, 17:31 Ryan Cavanaugh notifications@github.com wrote:

I'd say that's a strong argument for not producing the reverse map. Object.keys / Object.values would then produce a clean set of key names / values. Going from value -> key would be slightly awkward but I can't imagine why you'd need to do that.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub < https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-243623868 , or mute the thread < https://github.com/notifications/unsubscribe-auth/AAChnUl8PHgQ69whvb2xbzvHMkdzqOj2ks5qlMtngaJpZM4C9e4r

.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-243625089, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBOMGFSmm-RMOGOybVTbyv2kBSgB6ks5qlM01gaJpZM4C9e4r .

ghost commented 8 years ago

@RyanCavanaugh Would I be able to use the enum as an indexer? E.g.

enum S: string {
  A,
  B = "X",
  C
};
const foo = {} as { [ key: S ]: number };

It doesn't work if I use your "exactly equivalent" code. "An index signature parameter type must be 'string' or 'number'." (I suppose this actually applies to literal types in general.)

RyanCavanaugh commented 8 years ago

@errorx666 see #5683 for that

normalser commented 8 years ago

@RyanCavanaugh Any chance for this to land in 2.1.0 or is it further future ?

RyanCavanaugh commented 8 years ago

No idea. We just finished 2.0 and are still working out what's in the 2.1 roadmap.

dead-claudia commented 7 years ago

@RyanCavanaugh Any chance this will end up on the roadmap now?

zspitz commented 7 years ago

@RyanCavanaugh Does the idea of string enums (or enums of other primitive types) conflict with Typescript's design goals? Or is it just a matter of the right implementation?

NN--- commented 7 years ago

@zspitz What about using this technique ? http://angularfirst.com/typescript-string-enums/