Given an enum that looks like this:
export enum UsedProduct {
Yes = 'yes',
No = 'no',
Unknown = 'unknown',
}
I'd like to write a function that takes a set of string literals and returns an instance of UsedProduct
. So far, I wrote a function like this:
export function parseUsedProduct(usedProdStr: 'yes' | 'no' | 'unknown'): UsedProduct {
switch (usedProdStr) {
case 'yes':
return UsedProduct.Yes;
case 'no':
return UsedProduct.No;
case 'unknown':
return UsedProduct.Unknown;
default:
return unknownUsedProductValue(usedProdStr);
}
}
function unknownUsedProductValue(usedProdStr: never): UsedProduct {
throw new Error(`Unhandled UsedProduct value found ${usedProdStr}`);
}
This implementation isn't great because I have to redefine the possible values of the enum. How can I rewrite this function so that I don't have to define 'yes' | 'no' | 'unknown'
?
To convert a string to an enum: Use keyof typeof to cast the string to the type of the enum. Use bracket notation to access the corresponding value of the string in the enum.
In a string enum, each member has to be constant-initialized with a string literal, or with another string enum member. While string enums don't have auto-incrementing behavior, string enums have the benefit that they “serialize” well.
In summary, to make use of string-based enum types, we can reference them by using the name of the enum and their corresponding value, just as you would access the properties of an object. At runtime, string-based enums behave just like objects and can easily be passed to functions like regular objects.
In typescript, there are numerous ways to convert a string to a number. We can use the '+' unary operator , Number(), parseInt() or parseFloat() function to convert string to number.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With