Typescript: Generic function with enums
Asked Answered
B

3

8

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?

Burkhardt answered 13/9, 2021 at 9:39 Comment(0)
G
1

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.

Gley answered 13/9, 2021 at 10:14 Comment(6)
Two questions about your answer: (1) It is possible to use a unknown type as argument? The function should return the enum value (if it exists), otherwise undefined. For example: let input : unknown = 'blue'; let result = getParam(color, input);. (2) It is possible to return the type Partial<keyof typeof color>; (in case of colors) and Partial<keyof typeof coverage>; and so on? Of course only if the key exists, otherwise it should be undefined.Burkhardt
It is completely changes the logic. DO you want to make super generic getParam function?Gley
This comes from my old JS setup (maybe take a look at my getter functions : #69130003). I've to check user input which is unknown. Also I dont want to create a function for all of my global parameters (my enums).Burkhardt
if user input is unknown - we can remove all enums related logic, right?Gley
My approach is the following: a websocket emmits data to my client which is unknown. The only thing I know is the component and the slot name which is targeted. Imagine, the slot name is color. I've to check if a valid color is passed to that slot. At this point I want to be sure the data is correct and work with this correct data - for example after checking if the color value is valid, I want that the color variable has the type Partial<keyof typeof color>. So you can compare this to a select box / dropdown menu. Because of that I need the enums and my global properties (valid values).Burkhardt
@Burkhardt made an update. you can pass any object. if it does not work for you please provide your case scenarios an the questionGley
V
5

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

Vigil answered 13/9, 2021 at 10:8 Comment(0)
G
1

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.

Gley answered 13/9, 2021 at 10:14 Comment(6)
Two questions about your answer: (1) It is possible to use a unknown type as argument? The function should return the enum value (if it exists), otherwise undefined. For example: let input : unknown = 'blue'; let result = getParam(color, input);. (2) It is possible to return the type Partial<keyof typeof color>; (in case of colors) and Partial<keyof typeof coverage>; and so on? Of course only if the key exists, otherwise it should be undefined.Burkhardt
It is completely changes the logic. DO you want to make super generic getParam function?Gley
This comes from my old JS setup (maybe take a look at my getter functions : #69130003). I've to check user input which is unknown. Also I dont want to create a function for all of my global parameters (my enums).Burkhardt
if user input is unknown - we can remove all enums related logic, right?Gley
My approach is the following: a websocket emmits data to my client which is unknown. The only thing I know is the component and the slot name which is targeted. Imagine, the slot name is color. I've to check if a valid color is passed to that slot. At this point I want to be sure the data is correct and work with this correct data - for example after checking if the color value is valid, I want that the color variable has the type Partial<keyof typeof color>. So you can compare this to a select box / dropdown menu. Because of that I need the enums and my global properties (valid values).Burkhardt
@Burkhardt made an update. you can pass any object. if it does not work for you please provide your case scenarios an the questionGley
D
1

Inspired by the answers here and after several tries, I've found the best practice and the most simplified version for me so far.

enum MyEnum {
    Value1 = 'one',
    Value2 = 'two',
};

let traverse_enum = <ENUM_AS_TYPE>(ENUM_AS_VALUE: Record<string, ENUM_AS_TYPE>): ENUM_AS_TYPE => {
    let key: string = '';
    for (key in ENUM_AS_VALUE) {
        console.log(`${key}: ${ENUM_AS_VALUE[key]}`);
    }
    return ENUM_AS_VALUE[key];
};
traverse_enum<MyEnum>(MyEnum);

I think what's most important is figuring out that enum can be used as both a value and a type, which will allow it to be integrated into the JavaScript code after compiling, while most other TypeScript features will be completely removed afterward.

It's a pretty confusing point, and you need a bridge to tell TypeScript the relation between the enum as a type and the enum as a value. I use Record<string, ENUM_AS_TYPE> here. So the solution to your question is:

function getParam<ENUM_AS_TYPE>(value: string, ENUM_AS_VALUE: Record<string, ENUM_AS_TYPE>): ENUM_AS_TYPE | undefined {
    if(value in ENUM_AS_VALUE) return value as ENUM_AS_TYPE;
    return undefined;
}

A complete solution to your question at TS Playground

Dachi answered 24/3, 2024 at 6:52 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.