-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Enums behave unexpectedly, the workaround is verbose. There should be a less verbose way. #32690
Comments
Please don't close this issue as "working as intended" or "question" or "won't fix". At the very least, leave it open so people can vote on it. It's pretty annoying that enums are basically just There are probably a bunch of other issues related to this. The point being that enums, as they are now, are only really useful if you use bit masks and don't care about variables of that type being any number. However, that runs counter to how one usually thinks about enums. If anything, people that use bitwise operators on numeric enum values should get back a If backwards compatibility for this thing is so important, then, as above, new syntax for "proper" enums should be introduced that's a shorthand for the I don't have hard numbers but I feel like most people who use numeric enums expect it to behave like the Whenever someone asks me why their enum code isn't working (at work, on gitter, where ever), I just tell them because enums are broken and to not use them. It would be nice to have it unbroken Maybe introduce syntax like It would be nice if TS enum classes would have the behavior of the namespace-as-enum workaround. //Maybe call it `enum namespace`? `enum interface`?
enum class MyEnum {
Zero,
One,
}
//OK
const x : MyEnum.One = 1;
//Error
const y : MyEnum.One = 0;
//OK
const a : 1 = MyEnum.One;
//Error
const b : 1 = MyEnum.Zero;
//Error
const e : MyEnum = 2; |
Yep, we only need a syntax sugar (I know it might be much harder done than said) for the namespace-as-enum workaround. The behavior of the workaround should remain (it works 100% fine).. |
i think the |
Anyway, one reason for wanting a new type of enum Test {
One = "one",
Two = "two"
}
//Expected: "one"
//Actual : never
type strExtractEnum = Extract<"one", Test.One>
//Expected: Test.One
//Actual : Test.One
type enumExtractStr = Extract<Test.One, "one">
//Expected: OK
//Actual : Error
const e : Test.One = "one";
//Expected: OK
//Actual : OK
const s : "one" = Test.One;
//"one"
console.log(Test.One);
//"one"
console.log("one");
//true
console.log("one" === Test.One); |
This behavior is really counter-intuitive. The TypeScript handbook itself states that if all enum members are initialized to a static value,
This is simply not true, but I wish it was. What concerns me the most is that this behavior is in conflict with certain type inference rules such as exhaustiveness checking when using For example, the following is considered exhaustive, but in reality isn't: enum Direction { Left = 1, Right = 2 }
function directionName(dir: Direction): string {
switch (dir) {
case Direction.Left: return "left";
case Direction.Right: return "right";
}
}
// both calls return undefined, even if that shouldn't be possible
console.log(directionName(Direction.Left | Direction.Right));
console.log(directionName(42)); I was aware that you can use enums as bit flags and combine them using bitwise logic operators, but I assumed that the compiler somehow distinguished between the two cases based on some conventions. For example, it could activate bit flag behavior only if all constants are initialized to a single bit or a combination of other constants, and otherwise fallback to a strict union type of all constants. enum DirectionFlags {
// single bits
Up = 1 << 0,
Right = 1 << 1,
Down = 1 << 2,
Left = 1 << 3,
// predefined combinations
UpRight = Up | Right,
DownRight = Down | Right,
DownLeft = Down | Left,
UpLeft = Up | Left
} It really think it would be much safer and more convenient to only allow bit flag usage if the enum definition meets certain conditions, to make sure it really was intended to be used in this way. I understand that changing the behavior of enums breaks backward compatibility. |
Just stumbled upon this and I have to say that I'm really surprised about the current behavior. To me, the keyword enum State {
Success = "SUCCESS",
Error = "ERROR",
}
const state: State = "SUCCESS"; // Error, because it's not State.Success
// This is how symbols work To me, this behavior is very unexpected and also inconsistent. And given the long list of issues (#17734, #21546, #11559, #15591, #8020, #18409, ...) I'm not the only one that finds that surprising. I do understand that this is a massive breaking change, especially for the TypeScript project itself. But I think that this needs to be addressed somehow. I honestly never used bit masks in my projects, but I do use enums all the time and there are a lot of places where I relied on the exhaustiveness check (which clearly doesn't work as you can pass in any number). What about a enum symbol State {
Success,
Error,
}
// Syntactic sugar for:
const State = {
Success: Symbol("Success") as unique symbol, // as unique symbol is currently not
Error: Symbol("Error") as unique symbol, // allowed but you probably get the gist :)
};
type State = typeof State.Success | typeof State.Error;
let state: State;
state = State.Success; // Works
state = "Success"; // Error, because it must be the State.Success symbol This would also be a good trade-off between easy to write enums that do not require you to re-spell the string literal (as string enums currently do) while maintaining debuggability because of symbol descriptions. |
I would also appreciate a proper fix for the enums. As they stand now, they seem a bit useless. enum MyEnum {
B = 3
}
function foo(param: MyEnum) {
}
foo(MyEnum.B); // This should be the only working example.
foo('asdfasdf'); // This does not work, which is good.
foo(2); // This should also not work, but it does. :( |
For the case in #32690 (comment) //Expected: "one"
//Actual : never
type strExtractEnum = Extract<"one", Test.One> A workaround for this is in the following gist: https://gist.github.com/forivall/8968ec4ef450ceac9949b3a25583ca6d It leverages the behaviour that an enum can be treated as a const value, and makes it work the other way around. |
I went with this: const roles = {
DSCO: 0x1 as 0x1,
AIDE: 0x2 as 0x2,
LT: 0x4 as 0x4,
FF: 0x8 as 0x8,
CHIEF: 0x12 as 0x12,
};
type Role = keyof typeof roles;
type RoleAsInt = typeof roles[keyof typeof roles]; It isn't completely DRY but it's close: the repetition is short and contained to each line. Would love some feedback. |
@hoclun-rigsep you can write |
Amazing! Cuts down on the RY. Already glad I posted. |
I'm surprised there isn't a simple way to create a typesafe enum distinct from The whole "bit bitfields are convenient" argument seems specious - assigning a number to a string is convenient, but wrong. using enums like this is a C-ism - fixed in C++. |
Current:
=> It clearly leads to an unsafe type as in many cases above. Suggestion:
Current solution:
|
@DanielRosenwasser Hey Dan, what do you think about this issue? A lot of people have to write more verbose code because of this. |
Bumped into this today as well, just for the record. |
@RyanCavanaugh Any breakthrough here? |
Seems like this is finally fixed in TypeScript v5! Thanks a lot for the work! 🎊 |
Search Terms
enums
Suggestion
Because enums do not work as expected...
...people start using this workaround (aka "namespace-as-enum"):
Maybe there should be a less verbose way to do this common stuff?
For example it could be something like
enum MyEnum {type Zero, type One}
Use Cases
I use number values (i.e. enums) extensively instead of string values for performance reasons (a lot of JSON.stringify/parse).
Examples
C++ had the same problem and they fixed it
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: