2

There have been some similar questions, like:

My example is different from the other linked questions because I am using a string literal which is different from the enum member name.

I have the following enum:

export enum AttackType {
  MELEE = <any>'close', // I had to use <any> to make it work at all
  RANGED = <any>'far'
}

I want to get the correct enum based on the assigned string literal. I thought AttackType['close'] would be the same as AttackType.MELEE, but it is not. The former prints MELEE, and the latter prints close, making the following statement false

AttackType['close']===AttackType.MELEE

So If I have the following class:

export class Hero{
 attackType: AttackType;
 constructor(){
  this.attackType = AttackType['close']; // no errors, and prints MELEE,
  this.attackType = AttackType.MELEE; // no errors, and prints close,
  this.attackType = AttackType[AttackType['close']]; // compile error.
  this.attackType = AttackType[<string>AttackType['close']]; // no errors, and prints close.

 }
}

I am wondering how I should tackle this problem. How can I make sure that the attackType is assigned properly when I only know the string literal (e.g. 'close')?

I could use the "strange" way (AttackType[<string>AttackType['close']]) to assign the correct enum value, making it be the same as AttackType.MELEE, but I am not sure if this a good way of doing it.

John
  • 10,165
  • 5
  • 55
  • 71

1 Answers1

4

I'm not happy about the idea of trying to fool the compiler into generating reverse mappings for string enums by using that <any> type assertion. The reverse string mappings are omitted on purpose, as @RyanCavanaugh said in a relevant GitHub issue:

If we provided the reverse map automatically (which, by the way, is not necessarily non-ambiguous!), then you'd have no way to distinguish keys from values unless there was a completely separate object created at runtime. It is trivial to write a function that constructs the reverse map if you want it; our goal is to emit as little code as possible so it makes sense to just emit the data you need (to keep code size down) and let you construct the reverse map on an as-needed basis.

I think if you continue using that trick, you will find yourself needing to do assertions like you're doing, and you shouldn't be surprised if some future version of TypeScript breaks it entirely.

But if a development lead for TypeScript says "it is trivial to write a function that constructs the reverse map", then we can try that and see how it goes. So how would you go about that? At runtime you really just have to iterate through the enum object entries and produce a new object with the keys and values switched. And if you want both the forward and reverse map in the same object, you can merge the properties from the regular enum into the reversed one:

type Entries<T extends object> = { [K in keyof T]: [K, T[K]] }[keyof T]

function reverseEnum<E extends Record<keyof E, string | number>>(
  e: E
): { [K in E[keyof E]]: Extract<Entries<E>, [any, K]>[0] };
function reverseEnum(
  e: Record<string | number, string | number>
): Record<string | number, string | number> {
  const ret: Record<string | number, string | number> = {};
  Object.keys(e).forEach(k => ret[e[k]] = k);
  return ret;
}

function twoWayEnum<E extends Record<keyof E, string | number>>(e: E) {
  return Object.assign(reverseEnum(e), e);
}

The signature for reverseEnum() involves a bit of type juggling. The type function Entries<T> turns an object type T into a union of key value pairs., e.g., Entries<{a: string, b: number}> evaluates to ["a",string] | ["b",number]. Then the return type of reverseEnum() is a mapped type whose keys come from the enum values, and whose values come from the key for corresponding entry by extracting it. Let's see if it works:

enum AttackType {
  MELEE = 'close',
  RANGED = 'far'
}

const TwoWayAttackType = twoWayEnum(AttackType);
// const TwoWayAttackType = {
//   close: "MELEE";
//   far: "RANGED";
// } & typeof AttackType

// might as well make a type called TwoWayAttackType also, 
// corresponding to the property values of the TwoWayAttackType object
type TwoWayAttackType = AttackType | keyof typeof AttackType

console.log(TwoWayAttackType.close); // "MELEE"
console.log(TwoWayAttackType[TwoWayAttackType.far]) // "far"

You can see that the value TwoWayAttackType has the same type as the AttackType enum constant, with extra properties {close: "MELEE", far: "RANGED"}. One wrinkle is that TypeScript does not automatically generate a type named TwoWayAttackType corresponding to the property types of the TwoWayAttackType constant, so if you want one we have to make it manually, as I've done above.

Now you should be able to make your class as desired with no type errors:

class Hero {
  attackType: TwoWayAttackType;
  constructor() {
    this.attackType = TwoWayAttackType['close']; 
    this.attackType = TwoWayAttackType.MELEE; 
    this.attackType = TwoWayAttackType[TwoWayAttackType['close']]; 
  }
}

Note that if this method works for you, you can always rename the values/types above so that what I'm calling TwoWayAttackType is just AttackType (and then perhaps what I'm calling AttackType would be something like OneWayAttackType or BaseAttackType).

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for the answer. After reading this I figured that omitting ‘’ and creating a simple function to set the desired enum value based on a string input is a better Idea. Thank you for the example as well. – John Jan 04 '19 at 18:00