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.