2

The TypeScript code below has errors about using a string index with an enum in some cases, but not in others. Also, in some environments, the code with "errors" runs anyway.

enum LMSRole {
  AccountAdmin = 'Account Admin',
  SupportConsultant = 'Support Consultant',
  StudentEnrollment = 'StudentEnrollment',
  TeacherEnrollment = 'TeacherEnrollment'
}

console.log(LMSRole.SupportConsultant) /* "Support Consultant" - OK */

// these role strings would be read from input
const roles: String[] = ['AccountAdmin', 'SupportConsultant']
console.log(roles.map(r => LMSRole[r]))
/* Error given, yet runs on (http://typescriptlang.org/play)...
 * Type 'String' cannot be used as an index type. */

 console.log(LMSRole['SupportConsultant']) /* "Support Consultant" - OK */
/* In this example, a type of 'String' is used as an index,
 * but there's no error. Why? */

See this in action at typescriptlang.org's Playground.

What I don't understand is why in the map function, I get the error about using the string index. However, using a string directly to get a single value from the enum works without error.

I also don't understand why the Playground reports an error, but it runs anyway. On my own computer, if I try to run it, it will fail before anything is executed.

UPDATE — When I first wrote this question, I didn't explicitly state that I want to use this enum with strings read from input. (Say, from a text file or stream.) I thought the line beginning const roles would express that intention, but I guess it wasn't clear. I've added a comment to the code to clarify. Would that fact prevent the use of suggested code like typing the array Array<keyof typeof LMSRole> or forcing TS to treat the array as const?

Mr. Lance E Sloan
  • 3,297
  • 5
  • 35
  • 50
  • 1
    you have an array of type `string` not `String` first of all (primitives not objects) see: [What is the difference between types String and string?](https://stackoverflow.com/questions/14727044/what-is-the-difference-between-types-string-and-string). But actually what you want is for `roles` to be an array of keys of `LMSRole`, so `const roles: Array = ['AccountAdmin', 'SupportConsultant']` – pilchard Jan 26 '22 at 22:10
  • (The above will both index the enum correctly, and guard against elements that are not valid keys.) – pilchard Jan 26 '22 at 22:17
  • The string array is meant to represent input read from something like a text file or a stream. Would using the type `Array` work with that? – Mr. Lance E Sloan Jan 28 '22 at 21:00
  • Also, what would happen if one of the input strings was not a valid key in the enum? Could it fail gracefully? – Mr. Lance E Sloan Jan 28 '22 at 21:19
  • Types are only enforced at compile time, so if you pass an arbitrary file at runtime the type won't be enforced and you'll possibly access the enum (which is compiled to a javascript object) with an undefined key. This is a decent argument for the `as const` solution in the answer as it pushes type errors to the point of enum access where you will want to implement an implicit guard which will both avoid typescript errors and ensure error catching at runtime. – pilchard Jan 28 '22 at 21:37

1 Answers1

3

The type string may have more specific types. The type could be specific strings.

So when you do this:

const roles: string[] = ['AccountAdmin', 'SupportConsultant']
console.log(roles.map(r => LMSRole[r]))

roles is an array of string types. But the LMSRole only has very specific strings that you can use as an index, so you get the error.

Where here:

console.log(LMSRole['SupportConsultant'])

Typescript knows, because you directly used a string literal, what the specific type of that string is, and knows it's a good one.

If you changed that to:

LMSRole['SomethingElse']
//Element implicitly has an 'any' type because expression of type '"SomethingElse"' can't be used to index type 'typeof LMSRole'.
//  Property 'SomethingElse' does not exist on type 'typeof LMSRole'.(7053)

Then you get an error that makes sense.

You can force typescript to treat your array as constants, which lets it infer the specific strings types like so:

const roles = ['AccountAdmin', 'SupportConsultant'] as const
console.log(roles.map(r => LMSRole[r])) // no error
Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 1
    You could also define a type like this `type RoleTypes = keyof typeof LMSRole` and use it to type the roles array instead. `const roles: RoleTypes[] = ['AccountAdmin', 'SupportConsultant']` – Olian04 Jan 26 '22 at 22:08
  • 1
    'no error' is not the same as accurately typed. With this solution the map will require a guard to ensure `r` is a keyof the enum. – pilchard Jan 26 '22 at 22:20
  • There's a lot of types `roles` could be, but `as const` is perfectly type safe in this case. Maybe the OP doesn't want the array to be typed to have all the keys? Maybe they want to ensure it only has "admin" category roles listed. Without knowing the intent here, recommending `(keyof typeof LMSRole)[]` is a matter of context, usage, and opinion. – Alex Wayne Jan 26 '22 at 22:24
  • That's fair, though the array needn't hold all the keys if typed that way, and it pushes the error to the index statement with an unguarded map rather than catching it in the array declaration. I agree though, matter of context and your description is clear. (though you haven't pointed out the OPs misuse of String) – pilchard Jan 26 '22 at 22:26
  • I was about to as an edit, then I saw your comment @pilchard. Thought that covered it :) Your suggestion is probably a good one, though, depending on how this array gets used and modified. – Alex Wayne Jan 26 '22 at 22:35