Search Terms: Object.values
Code
enum Role {
Admin = 'ADMIN',
Results = 'RESULTS',
Finish = 'FINISH',
Config = 'CONFIG'
}
Object.values(Role).forEach(role => {
console.log(typeof role); // prints 'string' four times
});
Object.values(Role).includes('hello'); // errors at build time
Expected behavior: Object.values(EnumType) at build time should be returning an array of the same type as the values of the EnumType.
Actual behavior: Object.values(EnumType).includes('enumValueType') errors out with
error TS2345: Argument of type '"enumValueType"' is not assignable to parameter of type 'EnumType'.
It appears as though, at build time, Object.values(EnumType) is parsed as returning an array of EnumTypes.
Error first encountered on Typescript 3.6.2. Still present as of typescript@next.
Playground Link: Playground link. Playground does not appear to be correctly using ES2017 and Typescript 3.6.2/3.7.0 are not available.
The error looks reasonable to me. Why are you checking for 'hello' instead of, say, Role.Config? If you check Object.values(Role).includes(Role.Config) there's no error.
Maybe you're not aware that the type named Role is not the type of the value named Role ? The value Role that exists at runtime is a mapping from keys to values, while the type named Role is the union of the value types. So I expect Object.values(Role) to produce an Array<Role>, which it does. That is, your "expected behavior" is what is actually happening.
Our real-world use case here is that the input to includes comes from user input.
Object.values should be returning an array of the values contained on that object. The value of Role.Config should be the string CONFIG, so there should be no error.
Perhaps more importantly, prior to 3.6.2, this functionality worked exactly as expected. It was the (supposedly non-breaking, were semver followed) update that broke this purely at build time.
Here's the relevant type definitions for Object.values():
interface ObjectConstructor {
values<T>(o: { [s: string]: T } | ArrayLike<T>): T[]; // first overload
values(o: {}): any[]; // second overload
}
In TS3.5 and below, Object.values(Role) couldn't see Role as an indexable type, so it invoked the second overload signature above and returned an Array<any>, at which point all type safety on the array elements was lost.
But TS3.6 included #31687, allowing enum objects to be seen as implicitly having index signatures. So, starting in TS3.6, enum Role {...} is now assignable to a value of type {[k: string]: Role}. This is probably exactly what you want. And this now allows Object.values(Role) to invoke the first overload signature above and return Array<Role>, which is also probably exactly what you want.
That means in TS3.5 the compiler happily allowed you to call Object.values(Role).includes("randomThing") as well as Object.values(Role).includes(12345) and Object.values(Role).includes({name: "teapot", size: "little", height: "short", width: "stout"}). That's really too permissive; it probably should be an error to look for pottery in an array of enum values.
Starting in TS3.6, you are now forced to deal with the fact that Array<T>.includes(val: T) accepts a T and not something wider. In this case, it wants you to check a Role and not a string. Think that's too restrictive? That was brought up as #32201 and there are reasons and workarounds mentioned and linked in there
The short answer here is you should probably widen Object.values(Role) to string[] before doing includes() on some random string, if that's what you want (e.g., (Object.values(Role) as string[]).includes("randomString")).
I really don't think what we are asking for is particularly complex or confusing. At runtime, Object.values(Role) returns an array of strings, because Role is an enum where each of the entries has a value of a string. We would like the build time to respect that. We have defined an enum with string values, so Object.values should be treated as an array of strings, or at the very least should not break when accessed as such.
In TS3.5 and below, Object.values(Role) couldn't see Role as an indexable type, so it invoked the second overload signature above and returned an Array
, at which point all type safety on the array elements was lost.
This is certainly disappointing, and it is nice to see an attempt to tighten things up a bit. But unfortunately they have not been tightened up correctly.
this now allows Object.values(Role) to invoke the first overload signature above and return Array
, which is also probably exactly what you want
No, we want Object.values(Role) to return an array of the values contained within each Role. Very explicitly not Array<Role>. That's what the "values" in Object.values is referring to. Role.Admin being one instance of Role, and 'ADMIN' being the value of that Role.
We would also like it if breaking changes were not made in a minor version update. If this breaking change is absolutely necessary, it should be made with TypeScript version 4.x, not in a minor version update. It's very notable that according to Dependabot, this version update saw an over 10% failure rate, despite being a minor update, when most other builds have less than 3%. Heck, even the 2.9.2 → 3.0.1 breaking change had a lower rate of failure than this one! Surely Microsoft can not consider that acceptable.
No, we want
Object.values(Role)to return an array of the values contained within eachRole. Very explicitly notArray<Role>
Except that's exactly what Array<Role> is. Role is, in practice, a union of the possible values of the Role enum, which can be seen here:
enum Role {
Admin = 'ADMIN',
Results = 'RESULTS',
Finish = 'FINISH',
Config = 'CONFIG'
}
declare let x: Role;
let y: 'ADMIN' | 'RESULTS' | 'FINISH' | 'CONFIG' = x; // ok
You wouldn't argue that if you had an array of type number[], you should be able to go .includes("foo"), right? That's obviously a category error. So your actual pain point comes from the fact that TS has literal types in addition to the standard primitives. Since Array is generic, how could you decide which uses of T in that type should be widened (if they're literals) vs. ones that shouldn't? There are cases where widening the generic type wouldn't be desirable, so TS is conservative and doesn't do it.
tl;dr: TS knows statically at compile time the exact string values contained in Role-the-object, so it gives you an Array<"ADMIN"|"RESULTS"|"FINISH"|"CONFIG">, or Array<Role> for short. You then get stung because literal types can't safely be widened in a generic context.
We would also like it if breaking changes were not made in a minor version update. If this breaking change is absolutely necessary, it should be made with TypeScript version 4.x, not in a minor version update.
For better or worse, TS doesn't do semantic versioning; see #31761
To reiterate: this all seems to be working as intended. You should change your code to something like
enum Role {
Admin = 'ADMIN',
Results = 'RESULTS',
Finish = 'FINISH',
Config = 'CONFIG'
}
const allRoles: string[] = Object.values(Role); // this is what I want, compiler!
allRoles.forEach(role => { console.log(typeof role); });
allRoles.includes('hello');
which communicates your intent to the compiler (allRoles should be a string[] and not an any[] or a Role[]), and works in both TS3.5 and TS3.6. I'll stop spamming contributing to this thread now and wait for official word from a TS repo maintainer. Good luck!
The original PR suggests a workaround: Since numeric enums use older, looser rules than string enums, you can get close to the old behaviour by adding an entry to Role like Dummy = 0 (or your favourite number).
However, anytime something comes from user input, you should expect to assert or narrow its type before the compiler understands it. That code will necessarily involve some bending of the type system since the checks are runtime, but the errors you're getting are at compile-time. For 3.6, your checks that strings from user input are actually members of an enum now need associated assertions to make the type system leave you alone until the check is done. I would personally write it as
Object.values(Role).includes('hello' as Role)
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
Most helpful comment
The original PR suggests a workaround: Since numeric enums use older, looser rules than string enums, you can get close to the old behaviour by adding an entry to
RolelikeDummy = 0(or your favourite number).However, anytime something comes from user input, you should expect to assert or narrow its type before the compiler understands it. That code will necessarily involve some bending of the type system since the checks are runtime, but the errors you're getting are at compile-time. For 3.6, your checks that strings from user input are actually members of an enum now need associated assertions to make the type system leave you alone until the check is done. I would personally write it as