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

String valued members in enums #15486

Merged
merged 22 commits into from
May 17, 2017
Merged

String valued members in enums #15486

merged 22 commits into from
May 17, 2017

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg 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.

A = 123,
}
declare enum E03 {
A = hello,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is supposed to be the string "hello"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Now fixed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahejlsberg
Copy link
Member Author

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

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

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

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

Draqir commented May 24, 2017

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

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

+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.

@faceach
Copy link

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

@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.
Labels
API Relates to the public API for TypeScript Breaking Change Would introduce errors in existing code
Projects
None yet
Development

Successfully merging this pull request may close these issues.