Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parse string as Typescript Enum

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'?

like image 726
Jim Englert Avatar asked Sep 17 '18 14:09

Jim Englert


People also ask

How do I convert a string to enum in TypeScript?

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.

Can enum be string?

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.

How do I use string enums?

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.

How do I convert a string to a number in TypeScript?

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.


1 Answers

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!

like image 109
jcalz Avatar answered Nov 12 '22 01:11

jcalz