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

Assign string literals to string enum type #17690

Closed
domoritz opened this issue Aug 9, 2017 · 20 comments
Closed

Assign string literals to string enum type #17690

domoritz opened this issue Aug 9, 2017 · 20 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@domoritz
Copy link

domoritz commented Aug 9, 2017

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

Knagis commented Aug 9, 2017

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

@kitsonk
Copy link
Contributor

kitsonk commented Aug 9, 2017

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

@Knagis
Copy link
Contributor

Knagis commented Aug 9, 2017

@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;

@domoritz
Copy link
Author

domoritz commented Aug 9, 2017

Thank you @Knagis. You're right that I used the wrong constant and I corrected my original post.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Aug 9, 2017
@RyanCavanaugh
Copy link
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.

@domoritz
Copy link
Author

domoritz commented Aug 9, 2017

Thanks for the explanation @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 @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.

@domoritz domoritz closed this as completed Aug 9, 2017
@RyanCavanaugh
Copy link
Member

I recommend this approach: #3192 (comment)

@bobvanderlinden
Copy link

bobvanderlinden commented Oct 19, 2017

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?

@RyanCavanaugh
Copy link
Member

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

@bobvanderlinden
Copy link

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

@JoshuaKGoldberg
Copy link
Contributor

@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

@RyanCavanaugh
Copy link
Member

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

namespace BindingScope {
  export const Singleton: "Singleton" = "Singleton";
}

@JoshuaKGoldberg
Copy link
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 😦.

@RyanCavanaugh
Copy link
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"
}

@widdde
Copy link

widdde commented Dec 13, 2017

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.

@RyanCavanaugh
Copy link
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;
}

@widdde
Copy link

widdde commented Dec 13, 2017

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

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

@widdde
Copy link

widdde commented Dec 13, 2017

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

@m1gu3l
Copy link
Contributor

m1gu3l commented Jan 29, 2018

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 }

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

8 participants