mikl
mikl

Reputation: 24307

Is there a more elegant way of getting a value from a TypeScript enum, if it exists?

If I have an Enum of allowable values, like:

enum PackagingUnits {
  Be = 'becher',
  Bg = 'bogen',
  Bi = 'bidon'
}

I want a function to pass a string in to get an enum value, by key, so getPackagingUnitName('Be') would return 'becher'.

The naïve version of this would be:

function getPackagingUnitName(key: string): PackagingUnits | null {
  if (PackagingUnits[key]) {
    return PackagingUnits[key]
  }
  return null
}

However, TypeScript (version 3.3.3 atm) does not like this, and complains that Element implicitly has an 'any' type because index expression is not of type 'number'. for every instance of PackagingUnits[key].

I have tried a few variations of the if-condition (like Object.keys(PackagingUnits).includes(key), that makes the if-condition error-free, but this still gives the same error for the return statement, return PackagingUnits[key].

return PackagingUnits[key as PackagingUnits] does not work either.

The only working syntax I have been able to come up with is:

function getPackagingUnitName(key: string): PackagingUnits | null {
  const puIndex = Object.keys(PackagingUnits).indexOf(key)
  if (puIndex !== -1) {
    return Object.values(PackagingUnits)[puIndex]
  }
  return null
}

Is this really the best possible way to do this?

N.B.: Yes, this could be done easier if PackagingUnits was not an enum, but it needs to be so for other uses.

Upvotes: 0

Views: 1929

Answers (2)

jcalz
jcalz

Reputation: 329953

TypeScript doesn't treat access of an unknown property as a way of doing control flow narrowing to assert the existence of that property. You can only access properties on an object type if the compiler knows there is (or might be, in the case of optional properties) a property at that key. So as soon as you do PackagingUnits[key] where key is an arbitrary string, the compiler complains that PackagingUnits doesn't have a string index.

The easiest way to fix this is to use a type assertion and move on:

function getPackagingUnitName(key: string): PackagingUnits | null {      
  if ((PackagingUnits as any)[key]) { // assert your way out
    return PackagingUnits[key as keyof typeof PackagingUnits]; // assert your way out
  }
  return null;
}

This works but isn't guaranteed to be type safe by the compiler. You might not care, since the issue is inside an implementation of a function that other developers aren't going to mess with. If so, then great.


If you want the compiler to guarantee that what you are doing is type safe, you need to lead it through the analysis by careful steps. First you'd like the compiler to treat the PackagingUnits object not as something with three known keys whose values are PackagingUnits types, but instead, treat it as something which has a value at every string key, whose value is PackagingUnits | undefined. (This is as close as you can get to optional index signatures since TypeScript doesn't always distinguish missing properties from undefined-values properties). So you'd like this:

const PackagingUnitsIndexSignature: {[k: string]: PackagingUnits | undefined} =
  PackagingUnits; // error!

This should be safe (the type typeof PackagingUnits is a strict subtype of typeof PackagingUnitsIndexSignature) but the compiler doesn't realize it. The enum nature of PackagingUnits seems to confuse it. So we can do a two-step widening... first to a plain object type whose keys and values match that of the enum:

const PackagingUnitsPlainObject: Record<keyof typeof PackagingUnits, PackagingUnits> =
  PackagingUnits; // okay!
const PackagingUnitsIndexSignature: { [k: string]: PackagingUnits | undefined } =
  PackagingUnitsPlainObject; // okay!

Now we finally have something we can index into with a string without error:

function getPackagingUnitName(key: string): PackagingUnits | null {
  if (PackagingUnitsIndexSignature[key]) { // okay!
    return PackagingUnitsIndexSignature[key];  // error!!!!
  } else {
    return null;
  }       
}

Ugh, what happened? Shouldn't control flow narrowing work since we just checked the presence of a property at key? Unfortunately, bracket-type indexed access doesn't always narrow values the way we'd like. Rather than force this to work, let's just take it as a given that PackagingUnitsIndexSignature[key] returns PackagingUnits | undefined. If so, we should be able to use the logical "or" (||) operator to replace undefined with null:

function getPackagingUnitName(key: string): PackagingUnits | null {
  return PackagingUnitsIndexSignature[key] || null;  // okay
}

And the compiler is happy. To put all that together, here is a roundabout way to write the same logic so that the compiler confirms that it is type safe:

const PackagingUnitsPlainObject: Record<keyof typeof PackagingUnits, PackagingUnits> =
  PackagingUnits; // okay!
const PackagingUnitsIndexSignature: { [k: string]: PackagingUnits | undefined } =
  PackagingUnitsPlainObject; // okay!
function getPackagingUnitName(key: string): PackagingUnits | null {
  return PackagingUnitsIndexSignature[key] || null;  // okay!
}

Okay, hope that helps. Good luck!

Upvotes: 3

Nguyen Phong Thien
Nguyen Phong Thien

Reputation: 3387

Did you try

function getPackagingUnitName(key: keyof typeof PackagingUnits): PackagingUnits | null {
    if (PackagingUnits[key]) {
        return PackagingUnits[key];
    }
    return null
}

Of course, it is just in case, the key is got from an API or something like that. Otherwise the function is not really necessary. If you want a type check to ensure the key is one of the predefined values, just use

key: keyof typeof PackagingUnits;

Upvotes: 1

Related Questions