Typescript: Generic function with enums

Viewed 893

I am using several enums as global parameters.

enum color {
    'red' = 'red',
    'green' = 'green',
    'blue' = 'blue',
};
enum coverage {
    'none' = 'none',
    'text' = 'text',
    'background' = 'background',
};

I merge all enums to a type myEnums.

type myEnums = color | coverage;

Now I want to check and access values of the enums. For instance:

// Returns undefined if the argument value is not a color.
function getColor(value: string): color | undefined{
    if(value in color) return value as color;
    return undefined;
}

Because there are several enums, I want to create a generic function to access all of my enums. I tried the following:

function getParam<T extends myEnums>(value: string): T | undefined {
    if(value in T) return value as T;
    return undefined;
}

getParam<color>('red', color);        // => should return 'red'
getParam<coverage>('test', coverage); // => should return undefined

But the Typescript compiler says: 'T' only refers to a type, but is beeing used as a value here.'.

So I added an argument list: T to the function, but then Typescript assums that the argument list has the type string (and not object). The right-hand side of an 'in' expression must not be a primitive.

function getParam<T extends allEnums>(value: string, list: T): T | undefined {
    if(value in list) return value as T;
    return undefined;
}

So how can I call a generic function using T as an enum?

2 Answers

I think the key is a proper typing of T, you have to tell TS, that T is "enum-definiton-like" object. I've poked around and found this, more or less working

I have something like this in one of my projects:

export function parseEnum<E, K extends string>(
  enumDef: { [key in K]: E },
  str: string | undefined
): E | undefined {
  if (str && str in enumDef) {
    return enumDef[str as K] as E;
  }
  return undefined;
}

So, that's what works for "normal enum" and you want to use it "sum" of enums.

Fortunately, due to way enums are implemented in JS&TS, you can just merge enums definition objects like this:

const myEnums = {...coverage, ...color }
// typeof myEnums = coverage | color;

and it works:

const x = parseEnum(myEnums, 'x');
// typeof x = coverage | color | undefined;

Typescript playground here

enum is a special data structure in TypeScript.

If you want to put a restriction on generic you need to use typeof Enum instead.

In your example, you expect typeof color | typeof coverage and not color|coverage.

Because the last one is a union of values whether the first one is a union of enums.

COnsider this example:

enum color {
    'red' = 'red',
    'green' = 'green',
    'blue' = 'blue',
};
enum coverage {
    'none' = 'none',
    'text' = 'text',
    'background' = 'background',
};

type myEnums = color | coverage;

const hasProperty = <Obj, Prop extends PropertyKey>(obj: Obj, prop: Prop)
    : obj is Obj & Record<Prop, unknown> =>
    Object.prototype.hasOwnProperty.call(obj, prop);



function getParam<Keys extends PropertyKey, Value, E extends Record<Keys, Value>, Key extends keyof E>(enm: E, key: Key): E[Key]
function getParam<Keys extends PropertyKey, Value, E extends Record<Keys, Value>, Key extends PropertyKey>(enm: E, key: Key): Value | undefined
function getParam<E extends typeof color | typeof coverage, Key extends keyof E>(enm: E, key: Key): E[Key]
function getParam<E extends typeof color | typeof coverage, Key extends string>(enm: E, key: Key): undefined | E
function getParam<E extends typeof color | typeof coverage | Record<string, unknown>, Key extends keyof E>(enm: E, key: Key): E[Key] | undefined {
    return hasProperty(enm, key) ? enm[key] : undefined
}

const y = getParam(color, 'red');        // => color.red
const x = getParam(coverage, 'undefined'); // => undefined
const x2 = getParam({}, 'undefined'); // => unknown

const higherOrder = (key: string) => getParam(color, key)

const z = higherOrder('red') // typeof color | undefined

Playground

Please keep in mind, I'm not sure how you want to handle higher order functions so I just returned a union of enum and undefined in last example

As for this line if(value in T): T is a type and you can not treat is as a runtime value.

Related