116

Isn't it possible to set the type of a parameter to an Enum? Like this:

private getRandomElementOfEnum(e : enum):string{
    var length:number = Object.keys(e).length;
    return e[Math.floor((Math.random() * length)+1)];
}

Following error is thrown:

Argument expression expected.(1135)

With any obviously everything is alright:

private getRandomElementOfEnum(e : any):string{
    var length:number = Object.keys(e).length;
    return e[Math.floor((Math.random() * length)+1)];
}

Is there a possibility or a little workaround to define an enum as a parameter?

6
  • 3
    Shouldn't it be e: MyEnumType? Or do you want to accept any kind of enum. What good would that do? You can also do e: MyEnum|MyOtherEnum|MyThirdEnum . Commented Jun 11, 2015 at 7:58
  • Related (but possibly out-dated): stackoverflow.com/questions/21293063/… Commented Jun 11, 2015 at 8:00
  • every enum should be possible, so i can get ah random element of every enum by calling this function. Commented Jun 11, 2015 at 8:00
  • your seconds solution @Thilo works but isn't very handy, it should work for every enum. Commented Jun 11, 2015 at 8:04
  • @Thilo, No. MyEnum as a type offers its value type. If you want the enum itself to be passed, you need to think of it as an object. Commented Apr 6, 2020 at 14:13

11 Answers 11

46

It's not possible to ensure the parameter is an enum, because enumerations in TS don't inherit from a common ancestor or interface.

TypeScript brings static analysis. Your code uses dynamic programming with Object.keys and e[dynamicKey]. For dynamic codes, the type any is convenient.

Your code is buggy: length() doesn't exists, e[Math.floor((Math.random() * length)+1)] returns a string or an integer, and the enumeration values can be manually set…

Here is a suggestion:

function getRandomElementOfEnum<E>(e: any): E {
    var keys = Object.keys(e),
        index = Math.floor(Math.random() * keys.length),
        k = keys[index];
    if (typeof e[k] === 'number')
        return <any>e[k];
    return <any>parseInt(k, 10);
}

function display(a: Color) {
    console.log(a);
}

enum Color { Blue, Green };
display(getRandomElementOfEnum<Color>(Color));

Ideally, the parameter type any should be replaced by typeof E but the compiler (TS 1.5) can't understand this syntax.

Sign up to request clarification or add additional context in comments.

3 Comments

hehe, my code was actualy buggy, I realized it about 10min later, forgot to change the code here..
It is possible, with some extra enum-like types passing through a filter. But if it talks like enum and works like enum, why differ? Just <Enum extends Record<string, string | number>>(e: Enum) => { ... };
I was able to use Record<string, E> instead of any as type of the passing parameter
34

You can do better than any:

enum E1 {
    A, B, C
}
enum E2 {
    X, Y, Z
}

function getRandomElementOfEnum(e: { [s: number]: string }): number {
    /* insert working implementation here */
    return undefined;
}

// OK
var x: E1 = getRandomElementOfEnum(E1);
// Error
var y: E2 = getRandomElementOfEnum(window);
// Error
var z: string = getRandomElementOfEnum(E2);

6 Comments

The compiler assumes that E1 matches to the type { [s: number]: string }, but is it perennial? The object E1 contains string keys.
Number indexers don't constrain non-number key types
The compiler compiles this to: function getRandomElementOfEnum(e) { /* insert working implementation here */ return undefined; }
Note that even though TS does reverse mapping and one would probably think of these enums as "key "A" of E1 having value 0", [s: string]: number doesn't work.
Sort of works, but far from perfect.
|
23

Summing up the previous answers with some new syntax - a generic typesafe function, which works with numeric enums as well as string enums:

function getRandomElementOfEnum<T extends {[key: number]: string | number}>(e: T): T[keyof T] {
  const keys = Object.keys(e);

  const randomKeyIndex = Math.floor(Math.random() * keys.length);
  const randomKey = keys[randomKeyIndex];

  // Numeric enums members also get a reverse mapping from enum values to enum names.
  // So, if a key is a number, actually it's a value of a numeric enum.
  // see https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings
  const randomKeyNumber = Number(randomKey);
  return isNaN(randomKeyNumber)
    ? e[randomKey as keyof T]
    : randomKeyNumber as unknown as T[keyof T];
}

3 Comments

This should be the accepted answer, perfect solution, thank you so much
The only minor problem is, it's not restricted to enums. One can pass { p1: true, p2: { f: 8}} to it as well.
The safest type I could come up with is Record<string | number, string> | Record<string, number>. It still doesn't catch { p1: 5, p2, "f" }, but also flushes out some unexpected inputs.
19

I agree with @Tarh. Enums in TypeScript are just Javascript objects without a common interface or prototype (and if they are const enum, then they are not even objects), so you cannot restrict types to "any enum".

The closest I could get is something like the following:

enum E1 {
    A, B, C
}
enum E2 {
    X, Y, Z
}

// make up your own interface to match TypeScript enums
// as closely as possible (not perfect, though)
interface Enum {
    [id: number]: string
}

function getRandomElementOfEnum(e: Enum): string {
   let length = Object.keys(e).length / 2;
   return e[Math.floor((Math.random() * length))];
}

This works for all enums (without custom initializers), but it would also accept other arrays as input (and then fail because the method body relies on the very specific key structure that TypeScript enums have).

So unless you have a real need for such a "generic" function, make typesafe functions for the individual enum types (or a union type like E1|E2|E3) that you actually need.

And if you do have this need (and this might very well be an X-Y-problem that can be solved in a better, completely different way given more context), use any, because you have left typesafe territory anyway.

2 Comments

I haven't tested this theory but I'm pretty sure your implementation doesn't support enum E3 { X = 4, Y, Z }
it should be interface Enum {[key: number]: string | number} actually
12

Another possible option not mentioned above is using the actual values. This is however possible only when you know all the options. This, in my opinion is definitely better than any.

    doSomething(a: string, b: 'this'|'can'|'work'): void {
     //do something
    }

6 Comments

Yeah of course better than any, but this solution is pretty hard in maintenance. Everytime we edit the values of the enum we would need to remember where we used it and also refactor these parameters.
you saved me <3
Actually you can use this as the type for b: b: keyof typeof MyEnumType
@PabloMartínez Thats the best for this question. Post as an answer
@Ayyappa: It depends on the type of enum. If the enum is a string map it's not going to work. Like enum MyEnum { A = "admin", U = "user"} unfortunately won't work.
|
7

Tested on TypeScript 3.9.7

Solution

type EnumTypeString<TEnum extends string> =
    { [key in string]: TEnum | string; }

type EnumTypeNumber<TEnum extends number> =
    { [key in string]: TEnum | number; }
    | { [key in number]: string; }

type EnumType<TEnum extends string | number> =
    (TEnum extends string ? EnumTypeString<TEnum> : never)
    | (TEnum extends number ? EnumTypeNumber<TEnum> : never)

type EnumOf<TEnumType> = TEnumType extends EnumType<infer U>
    ? U
    : never

Usage

EnumType:
function forEachEnum<TEnum extends string | number>(
    enumType: EnumType<TEnum>,
    callback: (value: TEnum, key: string) => boolean|void,
) {
    for (let key in enumType) {
        if (Object.prototype.hasOwnProperty.call(enumType, key) && isNaN(Number(key))) {
            const value = enumType[key] as any
            if (callback(value, key)) {
                return
            }
        }
    }
}
EnumOf:
function forEachEnum2<TEnumType>(
    enumType: TEnumType,
    callback: (value: EnumOf<TEnumType>, key: string) => boolean|void,
) {
    for (let key in enumType) {
        if (Object.prototype.hasOwnProperty.call(enumType, key) && isNaN(Number(key))) {
            const value = enumType[key] as any
            if (callback(value, key)) {
                return
            }
        }
    }
}

Tests

enum EnumAsString {
    Value1 = 'value 1',
    Value2 = 'value 2',
}

enum EnumAsNumber {
    Value1 = 1,
    Value2 = 2,
}

// Error
let sn: EnumType<string> = EnumAsNumber

// Correct
let ns: EnumType<number> = EnumAsString // I have not found a solution for the error here
let nn: EnumType<number> = EnumAsNumber
let Nn: EnumType<EnumAsNumber> = EnumAsNumber
let ss: EnumType<string> = EnumAsString
let Ss: EnumType<EnumAsString> = EnumAsString

forEachEnum(EnumAsString, value => {
    let e: EnumAsString = value
    let s: string = value
    let n: number = value // Error
})

forEachEnum(EnumAsNumber, value => {
    let e: EnumAsNumber = value
    let s: string = value // Error
    let n: number = value
})

forEachEnum2(EnumAsString, value => {
    let e: EnumAsString = value
    let s: string = value
    let n: number = value // Error
})

forEachEnum2(EnumAsNumber, value => {
    let e: EnumAsNumber = value
    let s: string = value // Error
    let n: number = value
})

1 Comment

could you maybe provide some simple explanation? would be useful for not so experienced other users instead of only code
6

May be this trick will fit:

enum AbstractEnum { // put somewhere in hidden scope
}

private getRandomElementOfEnum(e: typeof AbstractEnum) {
    ...
}

1 Comment

I like this, and you can further strengthen the typing of the function by defining it as private getRandomElementOfEnum<T extends typeof AbstractEnum>(e: T): T[keyof T] {...}.
3

@selinathat's solution is great only if you have few types. but what if we have more ? for example :

doSomething(a: string, b: 'this'|'can'|'work'|'test1'|'test2'|'test3'): void {
 //do something
}

its pretty ugly hah !? i prefer to use keyof :

interface Items {
    'this',
    'can',
    'work',
    'test1',
    'test2',
    'test3',
}

doSomething(a: string, b: keyof Items): void {
 //do something
}

1 Comment

Check my comment in his answer: b: keyof typeof MyEnumType
2

Here is an example that allows passing an enum with a typechecked value of that enum using a generic. It's really a response to a slightly different question here that was marked as a duplicate: Typescript how to pass enum as Parameter

enum Color {
    blue,
};
enum Car {
    cadillac,
};
enum Shape {
    square,
}

type SupportedEnums = typeof Color | typeof Car;

type InvertTypeOf<T> = T extends typeof Color ? Color :
    T extends typeof Car ? Car : never;

function getText<T extends SupportedEnums>(enumValue: InvertTypeOf<T>, typeEnum: T) string | undefined {
  if (typeEnum[enumValue]) {
    return `${enumValue}(${typeEnum[enumValue]})`;
  }
}

console.log(getText(Car.cadillac, Car)); // 0(cadillac)
console.log(getText(0, Color)); // 0(red)
console.log(getText(4, Color)); // undefined

// @ts-expect-error Color is not Car
console.log(getText(Color.blue, Car));

// @ts-expect-error Car is not a Color
console.log(getText(Car.toyota, Color));

// @ts-expect-error  Shape is not in SupportedEnums
console.log(getText(5, Shape));

// @ts-expect-error  Shape is not in SupportedEnums
console.log(getText(Shape.square, Shape));

Comments

1

I made a helper type to accept any enum as a paramaeter, then you can handle whatever you need next with Object or by calling an index of the Enum.

type Enum = Record<string | number, string | number>

Now use it to accept any enum as parameter:

function enumValues<T extends Enum>(enum: T, filter?: "string"): string[];
function enumValues<T extends Enum>(enum: T, filter?: "number"): number[];
function enumValues<T extends Enum>(enum: T, filter?: undefined): (string|number)[];
function enumValues<T extends Enum>(enum: T, filter?: "string" | "number") {
  return Object.values(enum).filter(x => !filter || typeof x === filter);
}

enum color {
  red,
  green,
  blue
}

console.log(enumValues(color,"string"));
// output ['red','green','blue']

console.log(enumValues(color,"number"));
// output [0,1,2]

Comments

0

I had the same kind of problem, and i did this

  private getOptionsFromEnum(OptionEnum: Record<string, string>): Array<SelectOption> {
    return Object.keys(OptionEnum).map((value) => {
      return {
        name: OptionEnum[value],
        value,
      } as SelectOption;
    });
  }

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.