Parse string as Typescript Enum

TS4.1 ANSWER:

type UsedProductType = `${UsedProduct}`;

PRE TS-4.1 ANSWER:

TypeScript doesn't make this easy for you so the answer isn't a one-liner.

An enum value like UsedProduct.Yes is just a string or number literal at runtime (in this case, the string "yes"), but at compile time it is treated as a subtype of the string or number literal. So, UsedProduct.Yes extends "yes" is true. Unfortunately, given the type UsedProduct.Yes, there is no programmatic way to widen the type to "yes"... or, given the type UsedProduct, there is no programmatic way to widen it to "yes" | "no" | "unknown". The language is missing a few features which you'd need to do this.

There is a way to make a function signature which behaves like parseUsedProduct, but it uses generics and conditional types to achieve this:

type Not<T> = [T] extends [never] ? unknown : never
type Extractable<T, U> = Not<U extends any ? Not<T extends U ? unknown : never> : never>

declare function asEnum<E extends Record<keyof E, string | number>, K extends string | number>(
  e: E, k: K & Extractable<E[keyof E], K>
): Extract<E[keyof E], K>

const yes = asEnum(UsedProduct, "yes"); // UsedProduct.yes
const no = asEnum(UsedProduct, "no"); // UsedProduct.no
const unknown = asEnum(UsedProduct, "unknown"); // UsedProduct.unknown
const yesOrNo = asEnum(UsedProduct, 
  Math.random()<0.5 ? "yes" : "no"); // UsedProduct.yes | UsedProduct.no

const unacceptable = asEnum(UsedProduct, "oops"); // error

Basically it takes an enum object type E and a string-or-number type K, and tries to extract the property value(s) of E that extend K. If no values of E extend K (or if K is a union type where one of the pieces doesn't correspond to any value of E), the compiler will give an error. The specifics of how Not<> and Extractable<> work are available upon request.

As for the implementation of the function you will probably need to use a type assertion. Something like:

function asEnum<E extends Record<keyof E, string | number>, K extends string | number>(
  e: E, k: K & Extractable<E[keyof E], K>
): Extract<E[keyof E], K> {
  // runtime guard, shouldn't need it at compiler time
  if (Object.values(e).indexOf(k) < 0)
    throw new Error("Expected one of " + Object.values(e).join(", "));
  return k as any; // assertion
}

That should work. In your specific case we can hardcode UsedProduct:

type Not<T> = [T] extends [never] ? unknown : never
type Extractable<T, U> = Not<U extends any ? Not<T extends U ? unknown : never> : never>
function parseUsedProduct<K extends string | number>(
  k: K & Extractable<UsedProduct, K>
): Extract<UsedProduct, K> {
  if (Object.values(UsedProduct).indexOf(k) < 0)
    throw new Error("Expected one of " + Object.values(UsedProduct).join(", "));
  return k as any;
}

const yes = parseUsedProduct("yes"); // UsedProduct.yes
const unacceptable = parseUsedProduct("oops"); // error

Hope that helps. Good luck!


You can use the getKeyOrThrow-method from ts-enum-util. Not sure how it's implemented, but you can look at it here.

Here's a stackblitz I made to demonstrate the usage in your case.


With Typescript 4.1, it can be done in a simpler way

type UnionToEnum<E extends string, U extends `${E}`> = {
  [enumValue in E as `${enumValue & string}`]: enumValue
}[U]

enum UsedProduct {
  Yes = 'yes',
  No = 'no',
  Unknown = 'unknown',
}

function parseUsedProduct<K extends `${UsedProduct}`>(k: K): UnionToEnum<UsedProduct, K> {
  if (Object.values(UsedProduct).indexOf(k as UsedProduct) < 0)
    throw new Error("Expected one of " + Object.values(UsedProduct).join(", "));
  return k as UsedProduct as UnionToEnum<UsedProduct, K>;
}

// x is of type UsedProduct.Yes
let x = parseUsedProduct('yes');
// error
let c = parseUsedProduct('noo');

playground

The key here is `${UsedProduct}`, which removes the 'enumness' of the enum values and convert them to a string literal.

Caveat: This only works with string enum values, not number enum values.