40

Given a function that has an argument supposed to be an enum. The enum passed in can have different numbers of properties. How to fix the type of that argument ? enum itself is not a type.

E.g. :

function (myEnum: mysteriousType){
  //What is that mysteriousType ?
}


The use case is to build a generic method to instantiate dat.GUI options from an enum, whatever the string/number type in the enum.
Mouloud85
  • 3,826
  • 5
  • 22
  • 42
  • Did you [read about enums in Typescript](https://www.typescriptlang.org/docs/handbook/enums.html)? – crashmstr May 03 '18 at 15:10
  • Do you mean how to declare a function that accepts *any* value of any enumeration, or the enumeration as an object? I mean, given `enum myEnum { one, two, three }` how would you call your function? `fn(myEnum)`? or `fn(myEnum.one)`? – Oscar Paz May 03 '18 at 15:10
  • @OscarPaz `fn(myEnum)`. But that should also work with `fn(anyOtherRandomEnum)`. – Mouloud85 May 03 '18 at 15:13
  • You want it to work for numeric enums only, or string enums? Or mixed ones? – jcalz May 03 '18 at 15:15
  • @crashmstr seems I didn't read that part. The paragraph "enums at runtime" should answer that, if we first define the types for both numeric and string enums. – Mouloud85 May 03 '18 at 15:17
  • The doc actually does not answer that. Their example works if we already know the expected properties. – Mouloud85 May 03 '18 at 15:20
  • @jcalz in the problem I'm facing the solution should be able to handle all those cases yes. – Mouloud85 May 03 '18 at 15:33

2 Answers2

42

With what you've stated so far (needs to accept all string / numeric / heterogeneous enums), the closest I can do is something like this:

type Enum<E> = Record<keyof E, number | string> & { [k: number]: string };
function acceptEnum<E extends Enum<E>>(
  myEnum: E
): void { 
  // do something with myEnum... what's your use case anyway?
}

enum E { X, Y, Z };
acceptEnum(E); // works

I'm not sure what you're going to do with myEnum if all you know is that it's "some enum type", but I guess that's up to you to figure out.


How I came up with this: I examined a bunch of concrete enum types, and they seem to have properties with string keys and string or numeric values (the forward mapping), as well as a numeric index key with string values (the reverse mapping for numeric values).

const works: { X: 0, Y: 1, Z: 2, [k: number]: string } = E; // works

The language designers might have constrained this further, since the reverse mapping will only produce the specific numeric keys and string values seen in the forward mapping, but for some reason it's not implemented like that:

const doesntWork: { X: 0, Y: 1, Z: 2, [k: number]: 'X' | 'Y' | 'Z' } = E; // error
const alsoDoesntWork: { X: 0, Y: 1, Z: 2, 0: 'X', 1: 'Y', 2: 'Z' } = E; // error

So the tightest constraint I can put on an enum type is the above E extends Enum<E>.


Note that this code does not work for const enum types which don't really exist at runtime:

const enum F {U, V, W};
acceptEnum(F); // nope, can't refer to `F` by itself

And also note that the above type (E extends Enum<E>) allows some things it maybe shouldn't:

acceptEnum({ foo: 1 }); // works

In the above, {foo: 1} is plausibly a numeric enum similar to enum Foo {foo = 1} but it doesn't have the reverse mapping, and if you rely on that things will blow up at runtime. Note that {foo: 1} doesn't seem to have an index signature but it still matches an index signature implicitly. It wouldn't fail to match unless you added some explicit bad value:

acceptEnum({foo: 1, 2: 3}); // error, prop '2' not compatible with index signature

But there's nothing to be done here. As I mentioned above, the implementation of enum typing currently does not constrain the numeric keys as much as it can, so there seems to be no way at compile time to distinguish between an enum with a good reverse mapping and one without one.


Hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for this detailed and fast answer and the thorough investigation. Marked as correct for the moment. I'm surprised there is no simple way, since an enum object itself is not easy to handle. The use case is a function that loops through the properties of the enum which is already tricky enough, and in this use case only `enum` will be passed in. – Mouloud85 May 03 '18 at 17:03
  • Could you post example where you would use an emum as a generic type in an interface? – dwjohnston Jun 17 '19 at 06:28
  • 1
    Can you explain what the ` & { [k: number]: string };` part does? – dwjohnston Jun 17 '19 at 06:44
  • I don't understand your first question... I'm not the one who wanted to use an enum this way so I'm not sure what the example should be. As for the second question it means that `Enum` should have numeric keys whose values are strings, to represent the reverse mapping of an enum. – jcalz Jun 17 '19 at 13:33
  • `type Enum = Record & { [k: number]: keyof E }` can be quietly more precise, but it doesn't make much difference – Dima Karaush Nov 12 '20 at 20:46
  • {} not work it allow empty plain obj – jon Jan 30 '23 at 05:20
1

Depending on your use-case, this may work: (it's what I use)

export function GetEnumValues<T>(enumType: T) {
    // if your enum has string value, get rid of the "Exclude<>" wrapper
    type ValType = T extends {[k: string]: infer X} ? Exclude<X, string> : any;

    const entryNames = Object.keys(enumType).filter(key=>!/[0-9]+/.test(key[0]));
    return entryNames.map(name=>enumType[name] as ValType );
}

Usage:

enum Fruit {
    Apple,
    Grape,
    Pear,
}
const enumVals = GetEnumValues(Fruit);
// the type of enumVals is: Fruit[]

EDIT: On re-reading your question, it seems this is not quite what you were looking for. Keeping it here for reference though, as it is useful for functions that need to be able to return "instances" of the given enum passed in (and where you don't want to have to manually supply a type in brackets).

Venryx
  • 15,624
  • 10
  • 70
  • 96