Jean-Philippe Pellet
Jean-Philippe Pellet

Reputation: 60006

Accurate types for a lookup of values of a string enum in TypeScript

Suppose I have this string enum:

enum Color {
    None = "Q",
    Red = "R",
    Green = "G",
    Blue = "B",
}

Now, in TypeScript, string enums have no reverse mappings, so I can't write Color["Q"] to obtain a potential Color back. I tried declaring my own helper function:

type StringEnum = {[key: string]: string};
function lookup<E extends StringEnum>(stringEnum: E, s: string): keyof E | undefined {
    for (const enumValue of keysOf(stringEnum)) {
        if (stringEnum[enumValue] === s) {
            return enumValue;
        }
    }
    return undefined;
}

with keysOf being

function keysOf<K extends {}>(o: K): (keyof K)[];
function keysOf(o: any) { return Object.keys(o); }

… but actually, I don't know what the return type should be. This is meant to compile, but doesn't:

const color: Color = lookup(Color, "Q") || Color.None;

because the return type of the lookup function is now "None" | "Red" | "Green" | "Blue" | undefined instead of just Color | undefined.

Can this be solved well while preserving the type information?

Upvotes: 3

Views: 2177

Answers (1)

ford04
ford04

Reputation: 74710

Your lookup function returns an Enum string key, but you also provide the Enum value Color.None as short-circuit in case lookup returns undefined. So the types don't match here.

In general, string Enum keys can be typed like this:

type ColorKeys = keyof typeof Color // "None" | "Red" | "Green" | "Blue"
const blueKey: keyof typeof Color = "Blue" 

If you write const blueValue = Color.None, variable blueValue holds the Enum string value "Q", not the literal "None". So we can fix the const color assignment in the following way:

const color = lookup(Color, "Q") || "None";
// or with explicit type
const color: keyof typeof Color = lookup(Color, "Q") || "None";

Enum types cannot have an index signature, so I typed StringEnum as {[key: string]: any} here to make it compile.

Playground

Update: get typed enum value from lookup

If you want to pass in an unnarrowed string value to lookup and give it back as narrowed enum value type if existent (otherwise undefined), you can do it in a similar way to your first example:

function lookup<E extends StringEnum>(
  stringEnum: E,
  s: string
): E[keyof E] | undefined {
  for (const enumKey of keysOf(stringEnum)) {
    if (stringEnum[enumKey] === s) {
      // here we have to help the compiler
      return stringEnum[enumKey] as E[keyof E];
    }
  }
  return undefined;
}

// let's test it
const look = lookup(Color, "Q") // const look: Color | undefined

// for further understanding
type AllColorKeys = keyof typeof Color // = "None" | "Red" | "Green" | "Blue"
type AllColorValues = (typeof Color)[keyof typeof Color] // = Color
type IsColorSuperType = Color.Blue extends Color ? true: false // = true

In case of Color enum, the returned lookup type will give you back value Q with type Color | undefined, as the union of all possible Color enum values Color.None, Color.Red and so on will be of supertype Color. The defined enum construct itself has the type typeof Color, comparable to the static and instance side of a class.

Playground

Upvotes: 4

Related Questions