6

Given some enum MyEnum {ONE, TWO}, I wanted to write a function called like

useMyFun(MyEnum, MyEnum.ONE);

and I failed to type it properly. Now I have something like the following

type StringKeyOf<T> = Extract<keyof T, string>;
type EnumNumber<E> = Record<StringKeyOf<E>, number>;
function useMyFun<E extends EnumNumber<E>, V extends number=number> (
    anEnum: E,
    initialState: number) : {value: V, setValue: (v: V) => void}
{
    const [value, setValue] = useState<V>(initialState as V);
    //.... more stuff using both arguments omitted    
    return {value, setValue};
}

It's a react-hook, but this doesn't matter as all you need to compile it is a dummy

function useState<V>(initialState: V) {
    const result: [V, (v: V) => void] = [initialState, v => { }];
    return result;
}

It works (using the current typescript version), but it allows me to call useMyFun(MyEnum, -1) as well, which is wrong. Note that I only care about enums like above, i.e., enums with default numerical values, no specified values and no const modifier.

*I also need that the return type has value: MyEnum rather than number.


I know that the runtime value of MyEnum is {0: 'ONE', 1: 'TWO', ONE: '0', TWO: '1'} which means that the above typing is actually wrong. However, this was the only way how to get the first argument compile. The second argument when dealing with MyEnum should actually be 0 | 1, but I can't get it working.

I really need both the enum object and the value in the function. Can someone get the types right?

maaartinus
  • 44,714
  • 32
  • 161
  • 320
  • 1
    Are you trying to get the function's second argument to only allow a specific value from the enum? – Jacques ジャック Jun 17 '20 at 02:56
  • How invested are you in using enums? – rob3c Jun 17 '20 at 09:12
  • @JacquesジャックNo, I need to provide all enum values in the result to populate a select (not shown above). – maaartinus Jun 17 '20 at 13:37
  • @rob3c Here, enums are best as unlike `'ONE' | 'TWO'`, they provide the list of all values, which is what I need. – maaartinus Jun 17 '20 at 13:38
  • @maaartinus I disagree they’re best here, as you’ve already hit a significant limitation in parameter value restriction in your question. However, it seems you’ve already pre-decided what’s best, so I’ll leave you to it. Good luck! – rob3c Jun 18 '20 at 00:07

3 Answers3

1

Related question you can find here

There is another one approach to figure out whether initial value is valid or not.

As you already know, TS might treat enum as a number or as an object or typeof enum. In the similar way typescript treats classes.

We need some how to obtain numerical keys of enum.

Let's try to iterate through enum keys:

enum MyEnum {
    ONE,
    TWO
}

type Enumerate<Enum extends number | string> = keyof {
    [Prop in Enum]: Prop
}

// non generic version 
type Keys = keyof typeof MyEnum
type Enumerate2 = keyof {
    [Prop in Keys]: Prop
}


type Result = Enumerate<MyEnum> // MyEnum, not good

It does not work, because TS is smart enought to figure out that we are iterating over enum keys. Hence we are getting MyEnum instead of 0 | 1.

We can wrap Prop key into a string to cheat typescript.

enum MyEnum {
    ONE,
    TWO
}

type Enumerate<Enum extends number | string> = keyof {
    [Prop in `${Enum}`]: Prop
}

type Result = Enumerate<MyEnum> // "0" | "1"

Much better now. But it is still not what we want. It is impossible to extract number from a string in current version of typescript in generic way.

But we always can compare string to number which is wrapped in string during the comparison. I mean something like that: "0" extends ${number} ? ...` Above code is perfectly valid.

enum MyEnum {
    ONE,
    TWO
}

type Enumerate<Enum extends number | string> = keyof {
    [Prop in `${Enum}`]: Prop
}

type Result = Enumerate<MyEnum> // "0" | "1"


type Values<T> = T[keyof T]

type IsKeyValid<InitialValue extends number, Enum extends Record<string | number, string | number>> =
    `${InitialValue}` extends Enumerate<Values<Enum>> ? InitialValue : never

function useMyFun<
    Enum extends Record<string | number, string | number>,
    InitialValue extends number,
    >(anEnum: Enum, initialState: IsKeyValid<InitialValue, Enum>) { }

useMyFun(MyEnum, MyEnum.ONE) // ok
useMyFun(MyEnum, 0) // ok

useMyFun(MyEnum, -1) // error
useMyFun(MyEnum, NaN) // error

Playground

Enum - is infered type of enum

InitialValue - is infered type of second argument.

IsKeyValid - is an utility type which checks whether wrapped into string InitialValue is equal to allowed enum keys or not. If it equal - return InitialValue, otherwise return never

P.S. Related question with React component props

0

Let me preface this by saying that I highly advise against doing this, but for the sake of fun, here's a hack to get the behavior you're looking for. This solution effectively makes the enum's values into an opaque type.

enum _MyEnum {
  ONE,
  TWO,
}

declare const __brand: unique symbol;

type MyEnum = {
  [K in keyof typeof _MyEnum]: (typeof _MyEnum)[K] & {
    readonly [__brand]: never;
  };
};

const MyEnum: MyEnum = _MyEnum as any;

type StringKeyOf<T> = Extract<keyof T, string>;
type EnumNumber<E> = Record<StringKeyOf<E>, number>;

function useMyFun<E extends EnumNumber<E>, V extends E[keyof E]>(
  anEnum: E,
  initialState: V
): { value: V; setValue: (v: V) => void } {
  const [value, setValue] = useState<V>(initialState as V);
  //.... more stuff using both arguments omitted
  MyEnum.ONE.toFixed;
  MyEnum.ONE;
  return { value, setValue };
}

useMyFun(MyEnum, MyEnum.ONE);
useMyFun(MyEnum, -1);
lazytype
  • 927
  • 6
  • 11
0

your way have an question.When Enum has '0',it can pass 0 to the function.

type EnumToNumber<T extends number, arr extends number[] = []>
    = `${T}` extends `-${infer N}` ? never :
    T extends arr['length'] ? arr['length']
    : EnumToNumber<T, [...arr, 1]>

type SafeEnum<T extends Record<string | number, string | number>,
    >
    = {
        [key in keyof T]: T[key] extends number ? EnumToNumber<T[key]> : `${T[key]}`
    }[keyof T]

it can work with string enum.but i don't find a way to transform negative number string literal to negative number literal

enum E{
  A=0,
  B=1,
  C='0'
}
type E=SafeEnum<typeof E> //0,1,'0'