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.

zspitz commented 7 years ago

@NN There are two issues, one minor, and one major:

The benefits of inlining are that the library could provide a set of named constants which could be used from Typescript, even though they might not exist / be accessible at runtime.

NN--- commented 7 years ago

I lining is a general problem, it can be applied to everything.

3964

danielepolencic commented 7 years ago

@NN--- thanks for the link, very interesting. The only big difference between the technique you posted and an enum is that with the latter I can access the types as properties. As an example, this is how one would implement redux's actions in typescript:

export type INCREMENT_COUNTER = 'App/INCREMENT_COUNTER';
export const INCREMENT_COUNTER : INCREMENT_COUNTER = 'App/INCREMENT_COUNTER';

export type DECREMENT_COUNTER = 'App/DECREMENT_COUNTER';
export const DECREMENT_COUNTER : DECREMENT_COUNTER = 'App/DECREMENT_COUNTER';

export type IncrementCounterAction = {
    type: INCREMENT_COUNTER,
    by: number
};
export type DecrementCounterAction = {
    type: DECREMENT_COUNTER,
    by: number
};

type Action = IncrementCounterAction | DecrementCounterAction;

function reducer<S>(state: S, action: Action): S {
  switch(action.type) {
    case DECREMENT_COUNTER:
      //action has .type & .by correctly detect by the compiler 
  }
}

The same code can be easily converted to enums:

enum Actions {
  INCREMENT_COUNTER,
  DECREMENT_COUNTER
}

export type IncrementCounterAction = {
    type: Actions.INCREMENT_COUNTER,
    by: number
};
export type DecrementCounterAction = {
    type: Actions.DECREMENT_COUNTER,
    by: number
};

type Action = IncrementCounterAction | DecrementCounterAction;

function reducer<S>(state: S, action: Action): S {
  switch(action.type) {
    case Actions.DECREMENT_COUNTER:
      //action has .type & .by correctly detect by the compiler
  }
}

but it falls short when I use the workaround:

export const Actions = {
  INCREMENT_COUNTER: 'INCREMENT_COUNTER' as 'INCREMENT_COUNTER',
  DECREMENT_COUNTER: 'DECREMENT_COUNTER' as 'DECREMENT_COUNTER'
}
export type Actions = (typeof Actions)[keyof typeof Actions];

export type IncrementCounterAction = {
    type: Actions.INCREMENT_COUNTER, // ERROR. no way to get type INCREMENT_COUNTER
    by: number
};
export type DecrementCounterAction = {
    type: Actions.DECREMENT_COUNTER,  // ERROR. no way to get type DECREMENT_COUNTER
    by: number
};

type Action = IncrementCounterAction | DecrementCounterAction;

function reducer<S>(state: S, action: Action): S {
  switch(action.type) {
    case Actions.DECREMENT_COUNTER:
      //action has .type & .by correctly detect by the compiler
  }
}
rob3c commented 7 years ago

@danielepolencic You may be interested in my solution using ngrx. Here's an issue I created in their repo about updating their sample for typescript 2.1+. It shows pre- and post- typescript 2.1+ versions using discriminated unions. TS 2.1+ introduced a breaking change (arguable a bug fix) regarding how string literals are handled.

Anyway, here's the ngrx issue about it, and here's a direct link to the typescript playground referenced in that issue.

ghost commented 7 years ago

@danielepolencic Instead of

export type IncrementCounterAction = {
    type: Actions.INCREMENT_COUNTER, // ERROR. no way to get type INCREMENT_COUNTER
    by: number
};
export type DecrementCounterAction = {
    type: Actions.DECREMENT_COUNTER,  // ERROR. no way to get type DECREMENT_COUNTER
    by: number
};

try

export type IncrementCounterAction = {
    type: typeof Actions.INCREMENT_COUNTER,
    by: number
};
export type DecrementCounterAction = {
    type: typeof Actions.DECREMENT_COUNTER,
    by: number
};
danielepolencic commented 7 years ago

@errorx666 @rob3c nice! thanks!

There still a lot of boiler plate, but it's definitely usable.

👍

RyanCavanaugh commented 7 years ago

Some discussion today. Unorganized notes; refer to https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-240581743 for a mini-spec with some changes as follows

nevir commented 7 years ago
  • 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 commented 7 years ago

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 https://github.com/Microsoft/TypeScript/issues/1206#issuecomment-225813145) seem to be doing pretty well. And we're definitely not adding boolean enums :wink:

nevir commented 7 years ago

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 commented 7 years ago

Actually you can achieve that with

enum bool {
  true = 0,
  false = 1
}
nevir commented 7 years ago

Hah

On Tue, Apr 4, 2017 at 3:05 PM Daniel Busłowicz notifications@github.com wrote:

Actually you can achieve that with

enum bool { true = 0, false = 1 }

— 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-291645840, or mute the thread https://github.com/notifications/unsubscribe-auth/AAChndeXT4OF-SiF0Vd16Au4nZcYIGh9ks5rsr6fgaJpZM4C9e4r .

dead-claudia commented 7 years ago

@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 commented 7 years ago

@nevir Don't forget FileNotFound.

RyanCavanaugh commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

Implementation now available in #15486.

LMFinney commented 7 years ago

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 commented 7 years ago

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>