Skip to content

Assign string literals to string enum type #17690

Closed

Description

TypeScript Version: 2.4.0

Code

enum Colors {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE",
}

const c1: Colors = Colors.Red;
const c2: Colors = "RED";

Expected behavior:

I'd expect both lines with c1 and c2 to work but only the first compiles.

Actual behavior:

[ts] Type '"RED"' is not assignable to type 'Colors'.
const c2: Colors

Activity

  1. Knagis commented on Aug 9, 2017

    @Knagis
    Contributor

    The value of the enum should "RED", not "Red", the latter is the name of the value, not the value itself.

  2. kitsonk commented on Aug 9, 2017

    @kitsonk
    Contributor

    Kārlis Gaņģis (@Knagis) I think the OP is trying to point out that while the above doesn't work, the following does:

    enum Colors {
        Red = 1,
        Green = 2,
        Blue = 3,
    }
    
    const c1: Colors = Colors.Red;
    const c2: Colors = 1;

    It is an incongruence in the language.

  3. Knagis commented on Aug 9, 2017

    @Knagis
    Contributor

    Kitson Kelly (@kitsonk) I mainly pointed out that OP was using the wrong constant. It still does not let assign even with the correct one though.

    However in your example, unfortunately this also compiles: const c3: Colors = 123;

  4. domoritz commented on Aug 9, 2017

    @domoritz
    Author

    Thank you Kārlis Gaņģis (@Knagis). You're right that I used the wrong constant and I corrected my original post.

  5. RyanCavanaugh commented on Aug 9, 2017

    @RyanCavanaugh
    Member

    The intent of string enums is that the values of the enum are opaque (i.e. they can be changed behind the scenes without breaking consumers). If you don't want that behavior, it's easy enough to use the pre-string-enum workarounds to create similar union types.

    Numeric enums allow assignments from arbitrary numbers because we need to support bitflag enums and the | / & / etc operators return number. There's no corresponding behavior for string enums that needs to be supported.

    Why don't | and & return enum types when their operands are enum types? I have no idea.

  6. domoritz commented on Aug 9, 2017

    @domoritz
    Author

    Thanks for the explanation Ryan Cavanaugh (@RyanCavanaugh)! Can you point me to some documentation for the best practice workarounds from pre-string-enums? I'm currently using namespaces (https://github.com/vega/vega-lite/blob/6ea812df890070f4cc7f72ac3efc96eaa44a8c4e/src/type.ts#L4) because Daniel Rosenwasser (@DanielRosenwasser) suggested it to me last year. However, with this suggestion I have a namespace and a type with the same name if I want to use string literals and constants at the same time.

  7. RyanCavanaugh commented on Aug 9, 2017

    @RyanCavanaugh
    Member

    I recommend this approach: #3192 (comment)

  8. bobvanderlinden commented on Oct 19, 2017

    @bobvanderlinden

    Even though string-based enums are now in TypeScript 2.4, it still is not possible to assign a literal string to an string-based enum (even though the value of the literal string matches a value of the enum). See the OPs example in TypeScript Playground.

    It still fails on the following line:

    const c2: Colors = "RED";
    

    The example of OP is exactly the behavior I'd like to see: string literals and enum values being used interchangeably. It is however not yet possible.

    Taking it a step further, the following should work as well:

    enum ColorsEnum {
        Red = "RED",
        Green = "GREEN",
        Blue = "BLUE",
    }
    
    type ColorsType = 'RED' | 'GREEN' | 'BLUE';
    
    const c1: ColorsType = 'RED';
    const c2: ColorsEnum = c1;
    

    Should a new issue be created or can we reopen this one?

  9. RyanCavanaugh commented on Oct 19, 2017

    @RyanCavanaugh
    Member

    Bob van der Linden (@bobvanderlinden) this is the intended behavior, not a bug. String enum values are intentionally opaque to consumers; if you want a non-opaque string type, you should use a literal union (i.e. ColorsType).

  10. bobvanderlinden commented on Oct 22, 2017

    @bobvanderlinden

    Ryan Cavanaugh (@RyanCavanaugh) Thanks for your reply. I guess we'll have to discuss some more on swagger-codegen which of the two to use (enums or unions).

  11. JoshuaKGoldberg commented on Dec 11, 2017

    @JoshuaKGoldberg
    Contributor

    Ryan Cavanaugh (@RyanCavanaugh) IMO that's a limitation in the intended behavior that restricts the usage of string enums unnecessarily.

    Consider how InversifyJS uses a literal union to specify settings. Because the existing behavior is to take in a string for some settings, the only way consumers can specify them in a type-safe manner is with string literals:

    const container = new Container({
        defaultScope: "Singleton",
    });

    ...but that means we can't use slightly-easier-to-get-intellisense-for enum members:

    const container = new Container({
        defaultScope: BindingScope.Singleton,
    });

    Does that change your opinion?

    Relevant issue: inversify/InversifyJS#706

  12. RyanCavanaugh commented on Dec 11, 2017

    @RyanCavanaugh
    Member

    There's already a syntax available to you for that: exported consts in namespaces

    namespace BindingScope {
      export const Singleton: "Singleton" = "Singleton";
    }
  13. JoshuaKGoldberg commented on Dec 11, 2017

    @JoshuaKGoldberg
    Contributor

    That does work:

    type BindingScope = "Singleton" | "Transient" | "Request";
    
    namespace BindingScope {
        export const Singleton: "Singleton" = "Singleton";
        export const Transient: "Transient" = "Transient";
        export const Request: "Request" = "Request";
    }
    const testName = (_: BindingScope) => _;
    
    testName("Singleton");
    testName(BindingScope.Singleton);

    ...but it's ugly and uses the discouraged namespace syntax. I suppose we can use it... but it would be cleaner just to allow this in enums 😦.

  14. RyanCavanaugh commented on Dec 11, 2017

    @RyanCavanaugh
    Member

    The entire point of string enums was to provide for a way to represent opaque string types because there's existing syntax to provide non-opaque string types.

    If you don't want to use namespace, you can use const

    const BindingScope: {
      Singleton: "Singleton"
    } =
    /* advantage: copy-paste from above 🙄 */
    {
      Singleton: "Singleton"
    }
  15. widdde commented on Dec 13, 2017

    @widdde

    The string enum in typescript differs from many other languages.
    For example in Java you can write:
    enum Colors {
    RED("Red"), GREEN("Green"), BLUE("Blue")
    }
    and you can get the enum key by the value and reverse.

    When having a java-server and a REST API (JSON) with a typescript written frontend this is quite a pain.
    The java-server sends the KEY (RED) in the JSON response and in the gui we see the value "Red" because the enum is defined as enum Colors { RED = "Red"} in our typescript interface.
    This is ok, but when trying to send the enum key to the server again in some sort of request it is quite a pain to send the Key RED that the java server recognises as Colors.RED. It does not recogise it as Colors.RED when sending "Red".

    +1 for make the ability to reverse map from a value and get the key, something like Colors["Red] will get RED.

  16. RyanCavanaugh commented on Dec 13, 2017

    @RyanCavanaugh
    Member

    +1 for make the ability to reverse map from a value
    widdde

    enum Colors {
      red = "reddish",
      blue = "bloo",
      green = "green"
    }
    
    function keyFromValue(stringEnum: { [key: string]: string }, value: string): string | undefined {
      for (const k of Object.keys(stringEnum)) {
        if (stringEnum[k] === value) return k;
      }
      return undefined;
    }
  17. widdde commented on Dec 13, 2017

    @widdde

    Ryan Cavanaugh (@RyanCavanaugh) Yes i know, I have done exactly that solution as you suggest, but I think this is a hack because a string enum does not work both ways which I think is strange behavior where many other langugaes works different.

    I did it like this:
    string keyString: string = Object.keys(stringEnum).filter(key => stringEnum[key] === value)
    (Does not have the code in front of me, but something like that)
    Will return undefined if not found.

  18. RyanCavanaugh commented on Dec 13, 2017

    @RyanCavanaugh
    Member

    If the string enum worked both ways, you'd have no way to tell keys from values. Presumably you want keyFromValue("blue") to fail in the above example, right?

  19. widdde commented on Dec 13, 2017

    @widdde

    Some kind of valueOf is what I'm seeking for
    Colors.valueOf("Red") will return RED for example.
    Native, not self written functions. ;)

  20. m1gu3l commented on Jan 29, 2018

    @m1gu3l
    Contributor

    There's always this pattern:

    enum Mode {
    	A = "A",
    	B = "B"
    }
    
    function mode(m: Mode | keyof typeof Mode) {
    
    }
    
    mode(Mode.A); // ok
    mode("A"); // ok
    mode("C"); // error
    

    However it would be useful if we had a modifier to allow permissive string enum types.

    literal enum Mode { A, B }
    
  21. locked and limited conversation to collaborators on Jul 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions