admin 管理员组

文章数量: 1086019

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?

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?

Share Improve this question edited Mar 27 at 12:40 jonrsharpe 122k30 gold badges268 silver badges475 bronze badges asked Mar 27 at 12:33 Michał SadowskiMichał Sadowski 2,1791 gold badge13 silver badges26 bronze badges 3
  • Generally you don't want to allow assigning literal strings to enums, which are (pseudo-)nominal types. I don't know why you'd rather use an enum instead of an as const object if that's your use case. (github/microsoft/TypeScript/issues/17690) • Still, if you want to do that, you should widen your array to that string literal type before checking with indexOf, possibly as shown in this playground link. Does that fully address the q? If so I'll write an a or find a duplicate; if not, what am I missing? – jcalz Commented Mar 27 at 13:33
  • @jcalz Yes, it does, thank you. I shall just use an object then, even though it requires a bit of boilerplate to use it as a type. – Michał Sadowski Commented Mar 27 at 14:24
  • Okay I couldn't find a good duplicate so I'm writing up an answer. – jcalz Commented Mar 27 at 16:01
Add a comment  | 

1 Answer 1

Reset to default 1

The main intended use case for enums 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 enums 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.

Playground link to code

本文标签: typescriptTuple ordering elements of stringkeyed enumStack Overflow