0

I've ran into a little issue when working with enums in TypeScript. My scenario is this:

  • I have defined a string enum containing allowed values
  • I have defined a method that accepts any incoming value (of string type), and have to cast it to the said enum

The problem is that even after checking if the incoming value from the method, intellisense tells me that value is still type of string instead of the enum. How can I force value to be a type of AllowedValues?

Here is a proof-of-concept example:

/** enum */
enum AllowedValues {
    LOREM_IPSUM = 'lorem ipsum',
    DOLOR_SIT = 'dolor sir',
    AMET = 'amet'
}

/** @method */
function doSomething(value: string = AllowedValues.LOREM_IPSUM) {

    // If value is not found in enum, force it to a default
    if (!(Object as any).values(AllowedValues).includes(value))
        value = AllowedValues.LOREM_IPSUM;

    // Value should be of type `AllowedValues` here
    // But TypeScript/Intellisense still thinks it is `string`
    console.log(value);
}

doSomething('amet');    // Should log `amet`
doSomething('aloha');   // Should log `lorem ipsum`, since it is not found in `AllowedValues`

You can also find it on TypeScript playground.

Terry
  • 63,248
  • 15
  • 96
  • 118
  • const av = Object.keys(AllowedValues).find(k => AllowedValues[k] === 'dolor sir') as AllowedValues; console.log(av); – softbear Oct 05 '18 at 13:09

1 Answers1

1

There are a few things going on here. One is that TypeScript doesn't understand that Object.values(x).includes(y) is a type guard on y. It doesn't match the built-in ways that the compiler tries to narrow types, such as typeof, instanceof, or in checks. To help the compiler out, you can use a user-defined type guard to express that way of checking:

function isPropertyValue<T>(object: T, possibleValue: any): possibleValue is T[keyof T] {
  return Object.values(object).includes(possibleValue);
}

declare function onlyAcceptAllowedValues(allowedValue: AllowedValues): void;
declare const v: string;
if (isPropertyValue(AllowedValues, v)) {
  onlyAcceptAllowedValues(v); // v is narrowed to AllowedValues; it works!
}

So let's first change your function to this:

function doSomething(value: string = AllowedValues.LOREM_IPSUM) {    
  if (!(isPropertyValue(AllowedValues, value)))
    value = AllowedValues.LOREM_IPSUM;

  // TypeScript/Intellisense still thinks it is `string`
  console.log(value);
}

Uh oh, still not working.


The second thing going on: if you reassign the value of a variable, TypeScript essentially gives up on its narrowing. The compiler makes a considerable effort to understand the affect of control flow on variable types, but it's not perfect. So, even though we understand that assigning AllowedValues.LOREM_IPSUM to value makes it an AllowedValues, the compiler gives up and assumes that it is its original annotated type, which is string.

The way to deal with this is to make a new variable which the compiler understands will never be anything but an AllowedValues. The most straightforward way to do that is to make it a const variable, like this:

function doSomething(value: string = AllowedValues.LOREM_IPSUM) {    
  const allowedValue = isPropertyValue(AllowedValues, value) ? value : AllowedValues.LOREM_IPSUM;
  console.log(allowedValue);
}

In the above, the new variable allowedValue is inferred as an AllowedValues because it is set as either value if the type guard succeeds (at which point value is an AllowedValues), or AllowedValues.LOREM_IPSUM if the type guard fails. Either way, allowedValue is an AllowedValues.


So that would be my suggested change if you want to help the compiler understand things. Hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for the really detailed answer! Now that it makes sense to me: I didn't know that TypeSCript will default to the original annotated type if variable reassignment happens at any point. – Terry Oct 12 '18 at 21:44