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

String valued members in enums #15486

Merged
merged 22 commits into from May 17, 2017

Conversation

Projects
None yet
@ahejlsberg
Copy link
Member

ahejlsberg commented Apr 30, 2017

Enum types in TypeScript come in two flavors: Numeric enum types and literal enum types. An enum type where each member has no initializer or an initializer that specififes a numeric literal, a string literal, or a single identifier naming another member in the enum type is classified as a literal enum type. An enum type that doesn't adhere to this pattern (e.g. because it has computed member values) is classified as a numeric enum type.

With this PR we implement the ability for literal enum types to have string valued members.

enum Color {
    Red = "red",
    Green = "green",
    Blue = "blue"
}

The above declaration creates an enum object Color with three members Red, Green, and Blue. The members have corresponding new string literal enum types, Color.Red, Color.Green, and Color.Blue that are subtypes of the string literal types "red", "green", and "blue" respectively. Finally, the declaration introduces a type Color that is an alias for the union type Color.Red | Color.Green | Color.Blue.

When a string literal enum type is inferred for a mutable location, it is widened to its corresponding literal enum type (rather than being widened to type string).

const c1 = Color.Red;  // Type Color.Red
let c2 = Color.Red;    // Type Color

The above behavior exactly corresponds to the behavior for numeric literal enum types, only the values are strings instead of numbers. In fact, an enum literal type can contain any mix of numeric literal values and string literal values.

enum Mixed {
    A,
    B,
    C = "hi",
    D = 10,
    E,
    F = "bye"
}

The Mixed type above is an alias for Mixed.A | Mixed.B | Mixed.C | Mixed.D | Mixed.E | Mixed.F, which is a subtype of 0 | 1 | "hi" | 10 | 11 | "bye".

Enum members with string literal values must always specify an initializer, but enum members with numeric literal values may omit the initializer provided it is the first member or the preceding member has a numeric value.

In the code emitted for an enum declaration, string valued members have no reverse mapping. For example, the following code is emitted for the Mixed enum declaration above:

var Mixed;
(function (Mixed) {
    Mixed[Mixed["A"] = 0] = "A";
    Mixed[Mixed["B"] = 1] = "B";
    Mixed["C"] = "hi";
    Mixed[Mixed["D"] = 10] = "D";
    Mixed[Mixed["E"] = 11] = "E";
    Mixed["F"] = "bye";
})(Mixed || (Mixed = {}));

As is already the case, no code is generated for a const enum. Instead, the literal values of the enum members are inlined where they're referenced.

Fixes #1206.
Fixes #3192.

@ahejlsberg ahejlsberg added this to the TypeScript 2.4 milestone May 1, 2017

@ahejlsberg ahejlsberg requested a review from mhegazy May 1, 2017

A = 123,
}
declare enum E03 {
A = hello,

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser May 1, 2017

Member

I think is supposed to be the string "hello"

This comment has been minimized.

@basarat

This comment has been minimized.

@ahejlsberg

ahejlsberg May 1, 2017

Author Member

Indeed. Now fixed.

This comment has been minimized.

@ahejlsberg

This comment has been minimized.

Copy link
Member Author

ahejlsberg commented May 11, 2017

@mhegazy You want to take a look before I merge this?

@wizarrc

This comment has been minimized.

Copy link

wizarrc commented May 22, 2017

@aaronbeall enum<T = number>
I think this syntax is more appealing. It would allow more than just string default syntax, but if left blank, defaults to number. It would be overloading the generic defaults syntax for enums.

enum<string> Color {
  Red = 0, // Red = 0
  Green, // Green = "Green"
  Blue = 2 // Blue = 2}

So the following code:

enum<number> Color {
   Red = "Red", // Red = "Red"
   Green = 1, // Green = 1
   Blue // Blue = 2
}

would be the same as:

enum Color {
   Red = "Red", // Red = "Red"
   Green = 1, // Green = 1
   Blue // Blue = 2
}
@robertpenner

This comment has been minimized.

Copy link

robertpenner commented May 23, 2017

@ahejlsberg @wizarrc To me, a generic Enum is more useful than a mixed Enum. I would rather have enum<T = number> where T can be any type, not just string or number, than mixing strings and numbers in the Enum.

@wizarrc

This comment has been minimized.

Copy link

wizarrc commented May 23, 2017

@robertpenner I agree, and it should throw a lint warning if types are mixed (by setting an initializer) with generic enums under strict mode. As for specifying any type, that would be difficult to implement as property assessors only support string, number (positive integer only), and I think symbols. But who is to say that that wont increase over time, so by making it generic, it would make it more future proof, and add constraints to value T in your example in the d.ts file that defines the generic enum. I'm not sure disallowing mixed enums altogether is the right thing to do. I think the current PR is a good start, and should add generic enums later (future feature request???).

@aaronbeall

This comment has been minimized.

Copy link

aaronbeall commented May 23, 2017

Personally I don't see much value in enums of mixed type, either (I can't remember ever doing that). I like the generic idea but I don't know how it would fill in values for anything other than string (use key as value) or number (incrementing value) automatically. Obviously something like enum<User> couldn't be filled in.

@wizarrc

This comment has been minimized.

Copy link

wizarrc commented May 23, 2017

@aaronbeall unless the User implements some function name (symbol for uniqueness) that generates it in a user defined manner. Just throwing that out there. Think of it as a compile-time function instead of runtime, and wouldn't even go into the final output. I thought about that during my first post, but didn't really go there since I didn't think it was a much demanded feature.

@Draqir

This comment has been minimized.

Copy link

Draqir commented May 24, 2017

Great feature, would there be any possibility of implementing nested enums? Like instead of doing this:

const enum EnumDefinition {
    name = "X"
}

const enum EnumDefinitionGroup1 {
    A = "B1"
}

const enum EnumDefinitionGroup2 {
    A = "B2"
}

to do this:

const enum EnumDefinition {
    name = "X",
    Group1 {
        A = "B1"
    },
    Group2 {
        A = "B2"
    }
}

one can cheat a bit using periods

const enum EnumDefinition {
    name = "X",
    "Group1.A" = "B1",
    "Group2.A" = "B2"
}

but it's not really an optimal solution since one would have to access everything with EnumDefinition["Group1.A"] which doesn't look that clean but it gets the job done.

One can use static class members to achieve the same structure;

class EnumDefinition {
    static readonly __name = "X";
    static readonly Group1 = {
        A: "B1"
    };
    static readonly Group2 = {
        A: "B2"
    };
}

Of course one can't use the key "name" because it conflicts with the built in name nor does anything get inlined.

Regardless I'm very happy this has been merged.

@wizarrc

This comment has been minimized.

Copy link

wizarrc commented May 24, 2017

@Draqir I'm lost by your example. Is EnumDefinitionGroup1 the same as Group1? How do the groups relate to each other? Also, what value do nested enums provide?

@Draqir

This comment has been minimized.

Copy link

Draqir commented May 24, 2017

@wizarrc
EnumDefinitionGroup1 is the same as Group1 inside the enum definition, the groups belong to the EnumDefinition.

Benefit: avoid magical strings. The more times you need to repeat a thing, the higher the risks are that there'll be bugs, and misspelling a string is quite easy. So in order to avoid that I usually use "blueprints" like the following:

class TvChannel {
    static readonly Table = "TvChannelTable";
    static readonly RelatedChannels = {
        Category: "Category",
    };
    static readonly ChannelProperties = {
        Audience: "Audience"
    };
}

class Channels {
    static readonly Sport = "Sport";
}

So if I wanted to retrieve all TvChannels that are sport channels I would do something like this;

db(TvChannel.Table).conditions(TvChannel.RelatedChannels.Category, Channels.Sport)

However this could just be nicely inlined by the TypeScript compiler to

db("TvChannelTable").conditions("Category", "Sport")

and the whole class wouldn't even need to exist..

Currently I can achieve the same with either using wonky periods for groups or using very many enums.. Neither is really perfect but it's still a huge improvement

@wizarrc

This comment has been minimized.

Copy link

wizarrc commented May 24, 2017

@Draqir so it's like enum namespaces? If that's the case, I think that's an awesome idea!

@Draqir

This comment has been minimized.

Copy link

Draqir commented May 24, 2017

@wizarrc Actually I hadn't considered that, but yes, it's exactly as enum namespaces.

@wizarrc

This comment has been minimized.

Copy link

wizarrc commented May 24, 2017

@Draqir One more thought. Support enum flags with namespaces, where each namespace (nested group) is a compiler enforced mutually exclusive flag, but it all is represented as a single number. I'm not sure how well the compiler can enforce or if it's even possible because it's collapsed to a single type but I've seen several projects (i.e. angular2+) use enum flags to combine types (groups or namespaces) in their low level implementation for performance reasons. It would be nice to have more static checking on mutual exclusion to make sure both items are not selected inside the same group. Take a look at the NodeFlags enum https://github.com/angular/angular/blob/master/packages/core/src/view/types.ts

Maybe if namespaces would have a combined type for number enums, like EnumDefinition is a supertype of Group1 | Group2 or something along those lines.

@mhegazy

This comment has been minimized.

Copy link
Contributor

mhegazy commented May 24, 2017

enums and namespaces already merge.

enum EnumDefinition {
    name = "X",
}
namespace EnumDefinition {
    export enum Group1 {
        A = "B1"
    }
    export enum Group2 {
        A = "B2"
    }
}
@wizarrc

This comment has been minimized.

Copy link

wizarrc commented May 24, 2017

@mhegazy @Draqir Oops. It did not occur to me that merging namespace and enum is basically enum groups. Sounds like a win. Unfortunately I don't think there is a way to collapse that all into a single number enum like my example shown above from the Angular project. That would be a nice way to have great abstraction and still low level performance. Something that can only be done if it is a pure number enum.

@Draqir

This comment has been minimized.

Copy link

Draqir commented May 24, 2017

@mhegazy

It's very close but no cigar.

error TS2339: Property 'name' does not exist on type 'typeof EnumDefinition'.

(Todays tsc version)

and I can't use const on the first enum

const enum EnumDefinition {
    name = "X",
}
namespace EnumDefinition {
    export const enum Group1 {
        A = "B1"
    }
    export const enum Group2 {
        A = "B2"
    }
}

The things inside the namespace works good though

@mhegazy

This comment has been minimized.

Copy link
Contributor

mhegazy commented May 24, 2017

const enums are removed, so they can not merge with namespaces. so this only works with none-const enums.

@wizarrc

This comment has been minimized.

Copy link

wizarrc commented May 24, 2017

@mhegazy If they allowed nesting namespaces inside of enums, they should be able to allow const since nothing is being merged and everything is known upfront. This external merging is the problem.

Imagine this code:

const enum<flags> NodeFlags {
    None,
    CatRenderNode {
        TypeElement,
        TypeText,
    }
}

Then you could access the enum by NodeFlags.CatRendererNode.TypeElement and CatRendererNode is type TypeElement | TypeText. The output would be a single constant number as this code:

const enum NodeFlags {
    None = 0,
    TypeElement = 1 << 0,
    TypeText = 1 << 1,
    CatRenderNode = TypeElement | TypeText

where flags is either a subset of number or is a static class with a sequential number generator that generates flags at compile time.

@seansfkelley

This comment has been minimized.

Copy link

seansfkelley commented Jun 1, 2017

+1 for @aaronbeall's comment on something along the lines of string enum Foo {} syntax for autogenerating the string values from the identifiers. For those interested in same, you can also try out typescript-string-enums which is a tiny library that generates semantically identical (I think?) string-based enumerations in this manner with a minimum of awkward syntax to support it.

@seansfkelley seansfkelley referenced this pull request Jun 1, 2017

Closed

Replace enums with string literals #324

15 of 15 tasks complete

@basarat basarat referenced this pull request Jun 7, 2017

Open

String Values Enums #294

@kitsonk kitsonk referenced this pull request Jun 23, 2017

Closed

Upgrade to TypeScript 2.4 #189

14 of 15 tasks complete

@zspitz zspitz referenced this pull request Jun 28, 2017

Merged

Updated existing activex definitions #17472

6 of 6 tasks complete

@ikatyang ikatyang referenced this pull request Jul 10, 2017

Closed

String Enum v2.4.1 #17057

@faceach

This comment has been minimized.

Copy link
Member

faceach commented Jul 31, 2017

There should be a mistake in TS playground. When I type

enum Mixed {
    C = "hi",
    F = "bye"
}

According to this change, it should be Mixed["C"] = "hi", but it shows me wrong result: Mixed["hi"] = "C"

var Mixed;
(function (Mixed) {
    Mixed[Mixed["C"] = "hi"] = "C";
    Mixed[Mixed["F"] = "bye"] = "F";
})(Mixed || (Mixed = {}));
@ikatyang

This comment has been minimized.

Copy link
Contributor

ikatyang commented Jul 31, 2017

@faceach

It's currently v2.3.3 in playground, see console.log for the version.

See #17406 and #17353.

@Microsoft Microsoft locked and limited conversation to collaborators Jun 14, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.