I wanted to create an enum that would map strings that I receive from the backend into something we can consistently use across the frontend. Let's say it's something like:
enum KEY_ENUM {
DENY = "RESTRICTED_ACCESS",
ACCES = "ACCES_WITH_PRIVILEGE",
}
So far so good. The problem came when I needed to order the strings. To do so I created a tuple ordering the values:
const ORDER_OF_PRIVILEGES = [KEY_ENUM.ACCES, KEY_ENUM.DENY] as const
And then I wanted to take an array of objects that I would get from the backend and sort them by the level of privilege:
type KeyEnumValues = `${KEY_ENUM}`
type User = {
id: number,
privilege: KeyEnumValues
}
const usersToCheck = new Array<User>
const usersSortedByPrivilege = [...usersToCheck].sort((a, b)=>{
return ORDER_OF_PRIVILEGES.indexOf(a.privilege) > ORDER_OF_PRIVILEGES.indexOf(b.privilege) ? 1 : -1
})
Unfortunately, this results in an error:
Argument of type '"RESTRICTED_ACCESS" | "ACCES_WITH_PRIVILEGE"' is not assignable to parameter of type 'KEY_ENUM'.
Type '"RESTRICTED_ACCESS"' is not assignable to type 'KEY_ENUM'.
What am I missing here? The same operation with an object assigned as const instead of an enum works correctly. Is it even possible to create an ordered list of enums?
The main intended use case for enum
s is to treat them as nominal types so that you don't accidentally mix them up. For example, the following is intentionally an error in TypeScript:
enum Attribute {
DEXTERITY = "DEX",
CONSTITUTION = "CON"
}
enum Device {
PRINTER = "PRN",
CONSOLE = "CON"
}
const device: Device = Attribute.CONSTITUTION;
Here it is considered to be a mistake to allow assigning an Attribute
where a Device
is expected, even though at runtime it's just "CON"
, which is a valid string value for both enums. The intent of enum
s is to treat the values as "opaque" things and you should be dealing mostly with them by key. See this comment on microsoft/TypeScript#17690 for more information.
So "RESTRICTED_ACCESS" | "ACCES_WITH_PRIVILEGE"
is intentionally not assignable to the KEY_ENUM
type.
If you really care about the string values, it's an indication that maybe you really want to use a const
-asserted object instead of an enum
, as you mentioned.
Still, you can access the string literal types of the enums, using template literal types to serialize them, as you've done in KeyEnumValues
. And while KeyEnumValues
isn't not assignable to KEY_ENUM
, the reverse direction is assignable. That is, KEY_ENUM
is assignable to KeyEnumValues
(you can think of it like KEY_ENUM.DENY
is a special nominal subtype of "RESTRICTED_ACCESS"). So KeyEnumValues
is wider than KEY_ENUM
.
So then, if every KEY_ENUM
is assignable to KeyEnumValues
, why can't you search for a KeyEnumValues
element inside an array of KEY_ENUM
with indexOf()
?
That's because the TypeScript call signature type for indexOf()
is:
interface ReadonlyArray<T> {
indexOf(searchElement: T, fromIndex?: number): number;
}
And so you can only search for something narrower than the type of the array elements, not wider. But conceptually this is backwards from what people tend to want. There have been many GitHub issues opened on this topic: for example, see microsoft/TypeScript#54422.
And the underlying reason why this can't easily be done is that while it's easy to constrain generic types to be narrower than something, there's no native way to constrain them to be wider. That is, TypeScript lacks so-called lower bound generic constraints, as requested in microsoft/TypeScript#14520 (the issue most of the above issues get closed as duplicating).
If TypeScript had lower bound constraints (usually written as super
instead of extends
as in Java), then indexOf()
could be typed as
interface ReadonlyArray<T> {
indexOf<S super T>(searchElement: S, fromIndex?: number): number;
}
and then your call would succeed. But it doesn't, so the call fails. There are ways to try to force TypeScript to allow these calls, but the easiest thing to do is just widen the array type before calling indexOf()
:
const OOP: readonly KeyEnumValues[] = ORDER_OF_PRIVILEGES; // okay
const usersSortedByPrivilege = [...usersToCheck].sort((a, b) => {
return OOP.indexOf(a.privilege) > OOP.indexOf(b.privilege) ? 1 : -1
})
Now T
is just KeyEnumValues
and indexOf()
succeeds. And the widening of ORDER_OF_PRIVILEGES
to readonly KeyEnumValues[]
works because KeyEnumValues
is itself wider than KEY_ENUM
.
See Why does the argument for Array.prototype.includes(searchElement) need the same type as array elements? for a very similar question and answer involving includes()
, which has the same issue.