89

Consider the following typescript enum:

enum MyEnum { A, B, C };

If I want another type that is the unioned strings of the keys of that enum, I can do the following:

type MyEnumKeysAsStrings = keyof typeof MyEnum;  // "A" | "B" | "C"

This is very useful.

Now I want to create a generic type that operates universally on enums in this way, so that I can instead say:

type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<MyEnum>;

I imagine the correct syntax for that would be:

type AnyEnumKeysAsStrings<TEnum> = keyof typeof TEnum; // TS Error: 'TEnum' only refers to a type, but is being used as a value here.

But that generates a compile error: "'TEnum' only refers to a type, but is being used as a value here."

This is unexpected and sad. I can incompletely work around it the following way by dropping the typeof from the right side of the declaration of the generic, and adding it to the type parameter in the declaration of the specific type:

type AnyEnumAsUntypedKeys<TEnum> = keyof TEnum;
type MyEnumKeysAsStrings = AnyEnumAsUntypedKeys<typeof MyEnum>; // works, but not kind to consumer.  Ick.

I don't like this workaround though, because it means the consumer has to remember to do this icky specifying of typeof on the generic.

Is there any syntax that will allow me to specify the generic type as I initially want, to be kind to the consumer?

Stephan G
  • 3,289
  • 4
  • 30
  • 49
  • 3
    You're running into the type/value name space issue that everyone seems to hit at some point, and it's obnoxious to explain. The short answer to your question will be something like "no, the consumer will need to use `typeof MyEnum` if they want to refer to the thing with keys `A`, `B`, and `C`." – jcalz May 16 '18 at 18:04

5 Answers5

138

No, the consumer will need to use typeof MyEnum to refer to the object whose keys are A, B, and C.


LONG EXPLANATION AHEAD, SOME OF WHICH YOU PROBABLY ALREADY KNOW

As you are likely aware, TypeScript adds a static type system to JavaScript, and that type system gets erased when the code is transpiled. The syntax of TypeScript is such that some expressions and statements refer to values that exist at runtime, while other expressions and statements refer to types that exist only at design/compile time. Values have types, but they are not types themselves. Importantly, there are some places in the code where the compiler will expect a value and interpret the expression it finds as a value if possible, and other places where the compiler will expect a type and interpret the expression it finds as a type if possible.

The compiler does not care or get confused if it is possible for an expression to be interpreted as both a value and a type. It is perfectly happy, for instance, with the two flavors of null in the following code:

let maybeString: string | null = null;

The first instance of null is a type and the second is a value. It also has no problem with

let Foo = {a: 0};
type Foo = {b: string};   

where the first Foo is a named value and the second Foo is a named type. Note that the type of the value Foo is {a: number}, while the type Foo is {b: string}. They are not the same.

Even the typeof operator leads a double life. The expression typeof x always expects x to be a value, but typeof x itself could be a value or type depending on the context:

let bar = {a: 0};
let TypeofBar = typeof bar; // the value "object"
type TypeofBar = typeof bar; // the type {a: number}

The line let TypeofBar = typeof bar; will make it through to the JavaScript, and it will use the JavaScript typeof operator at runtime and produce a string. But type TypeofBar = typeof bar; is erased, and it is using the TypeScript type query operator to examine the static type that TypeScript has assigned to the value named bar.


Now, most language constructs in TypeScript that introduce names create either a named value or a named type. Here are some introductions of named values:

const value1 = 1;
let value2 = 2;
var value3 = 3;
function value4() {}

And here are some introductions of named types:

interface Type1 {}
type Type2 = string;

But there are a few declarations which create both a named value and a named type, and, like Foo above, the type of the named value is not the named type. The big ones are class and enum:

class Class { public prop = 0; }
enum Enum { A, B }

Here, the type Class is the type of an instance of Class, while the value Class is the constructor object. And typeof Class is not Class:

const instance = new Class();  // value instance has type (Class)
// type (Class) is essentially the same as {prop: number};

const ctor = Class; // value ctor has type (typeof Class)
// type (typeof Class) is essentially the same as new() => Class;

And, the type Enum is the type of an element of the enumeration; a union of the types of each element. While the value Enum is an object whose keys are A and B, and whose properties are the elements of the enumeration. And typeof Enum is not Enum:

const element = Math.random() < 0.5 ? Enum.A : Enum.B; 
// value element has type (Enum)
// type (Enum) is essentially the same as Enum.A | Enum.B
//  which is a subtype of (0 | 1)

const enumObject = Enum;
// value enumObject has type (typeof Enum)
// type (typeof Enum) is essentially the same as {A: Enum.A; B: Enum.B}
//  which is a subtype of {A:0, B:1}

Backing way way up to your question now. You want to invent a type operator that works like this:

type KeysOfEnum = EnumKeysAsStrings<Enum>;  // "A" | "B"

where you put the type Enum in, and get the keys of the object Enum out. But as you see above, the type Enum is not the same as the object Enum. And unfortunately the type doesn't know anything about the value. It is sort of like saying this:

type KeysOfEnum = EnumKeysAsString<0 | 1>; // "A" | "B"

Clearly if you write it like that, you'd see that there's nothing you could do to the type 0 | 1 which would produce the type "A" | "B". To make it work, you'd need to pass it a type that knows about the mapping. And that type is typeof Enum...

type KeysOfEnum = EnumKeysAsStrings<typeof Enum>; 

which is like

type KeysOfEnum = EnumKeysAsString<{A:0, B:1}>; // "A" | "B"

which is possible... if type EnumKeysAsString<T> = keyof T.


So you are stuck making the consumer specify typeof Enum. Are there workarounds? Well, you could maybe use something that does that a value, such as a function?

 function enumKeysAsString<TEnum>(theEnum: TEnum): keyof TEnum {
   // eliminate numeric keys
   const keys = Object.keys(theEnum).filter(x => 
     (+x)+"" !== x) as (keyof TEnum)[];
   // return some random key
   return keys[Math.floor(Math.random()*keys.length)]; 
 }

Then you can call

 const someKey = enumKeysAsString(Enum);

and the type of someKey will be "A" | "B". Yeah but then to use it as type you'd have to query it:

 type KeysOfEnum = typeof someKey;

which forces you to use typeof again and is even more verbose than your solution, especially since you can't do this:

 type KeysOfEnum = typeof enumKeysAsString(Enum); // error

Blegh. Sorry.


To recap:

  • This is not possible
  • Types and values blah blah
  • Still not possible
  • Sorry
starball
  • 20,030
  • 7
  • 43
  • 238
jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 6
    This is brilliant, thank you. Double thumbs up for the additional nuanced education. This makes me think that typescript might benefit from some kind of preprocessor which could (at least in this case) allow the code to look simpler. But that wouldn't be typescript itself. Thanks. For now, I think I will choose to just force the consumer to understand and use the "keyof typeof" construction. And I'm glad to have this page to reference for why! – Stephan G May 18 '18 at 18:08
  • I have found the answer! See my answer below! I would suggest removing `THIS IS NOT POSSIBLE` now :D – Akxe May 23 '20 at 15:31
  • @Akxe I don’t see how your answer addresses the question, which is specifically about the keys of the enum and not the values – jcalz May 23 '20 at 16:41
  • @jcalz Well you should now, you granted me an idea and that idea actually works! :D – Akxe May 23 '20 at 17:06
  • 2
    The specific question is “how can I write a type function that takes in the name of an enum and outputs the union of its keys, without requiring the user to write `typeof` before the name of the enum”. The answer is “you cannot; the name of the enum as a type describes its values and not its keys”. Your answer does not even seem to attempt to address this question. `keyof typeof MyEnum` is the type you want, but there’s no way to turn this into something like `type KeyofTypeof = keyof typeof E` because `E` must be a type while `typeof` expects a value, not a type. – jcalz May 23 '20 at 19:55
25

It actually is possible.

enum MyEnum { A, B, C };

type ObjectWithValuesOfEnumAsKeys = { [key in MyEnum]: string };

const a: ObjectWithValuesOfEnumAsKeys = {
    "0": "Hello",
    "1": "world",
    "2": "!",
};

const b: ObjectWithValuesOfEnumAsKeys = {
    [MyEnum.A]: "Hello",
    [MyEnum.B]: "world",
    [MyEnum.C]: "!",
};

// Property '2' is missing in type '{ 0: string; 1: string; }' but required in type 'ObjectWithValuesOfEnumAsKeys'.
const c: ObjectWithValuesOfEnumAsKeys = {  //  Invalid! - Error here!
    [MyEnum.A]: "Hello",
    [MyEnum.B]: "world",
};

// Object literal may only specify known properties, and '6' does not exist in type 'ObjectWithValuesOfEnumAsKeys'.
const d: ObjectWithValuesOfEnumAsKeys = {
    [MyEnum.A]: "Hello",
    [MyEnum.B]: "world",
    [MyEnum.C]: "!",
    6: "!",  //  Invalid! - Error here!
};

Playground Link


EDIT: Lifted limitation!

enum MyEnum { A, B, C };

type enumValues = keyof typeof MyEnum;
type ObjectWithKeysOfEnumAsKeys = { [key in enumValues]: string };

const a: ObjectWithKeysOfEnumAsKeys = {
    A: "Hello",
    B: "world",
    C: "!",
};

// Property 'C' is missing in type '{ 0: string; 1: string; }' but required in type 'ObjectWithValuesOfEnumAsKeys'.
const c: ObjectWithKeysOfEnumAsKeys = {  //  Invalid! - Error here!
    A: "Hello",
    B: "world",
};

// Object literal may only specify known properties, and '6' does not exist in type 'ObjectWithValuesOfEnumAsKeys'.
const d: ObjectWithKeysOfEnumAsKeys = {
    A: "Hello",
    B: "world",
    C: "!",
    D: "!",  //  Invalid! - Error here!
};

Playground Link


  • This work with const enum too!
Akxe
  • 9,694
  • 3
  • 36
  • 71
  • 1
    This is useful, thanks. But not a full answer to the question. Lots of good stuff in this thread though. – Stephan G May 28 '20 at 20:09
  • @StephanG Oh I see now, you want a type to transform enum to list of values or keys to be used in key indexes. This is not possible... The reason is that there is no `enum` type nor can an enum extend other enum. Thus you can not create `type = { [key in T]: any}`. – Akxe May 28 '20 at 21:54
21

There is a solution that doesn't require to create new generic types.

If you declare an enum

enum Season { Spring, Summer, Autumn, Winter };

To get to the type you only need to use the keywords keyof typeof

let seasonKey: keyof typeof Season;

Then the variable works as expected

seasonKey = "Autumn"; // is fine
// seasonKey = "AA" <= won't compile
Pascal Ganaye
  • 1,174
  • 12
  • 28
4

You can just pass a type instead of a value and the compiler won't complain. This you achieve with typeof as you pointed out.
Will be just a bit less automatic:

type AnyEnumKeysAsStrings<TEnumType> = keyof TEnumType;

Which you can use as:

type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<typeof MyEnum>;
Nico Jones
  • 41
  • 1
2

If I understand the OP question correctly and Akxe answer: Here is a possible further simplification. Use the typescript type utility. Record<Keys, Type>

https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type

e.g.

enum MyEnum { A, B, C };

type enumValues = keyof typeof MyEnum;
type ObjectWithKeysOfEnumAsKeys = Record<enumValues, string>
const a: ObjectWithKeysOfEnumAsKeys = {
    A: "PropertyA",
    B: "PropertyB",
    C: "PropertyC",
};
redevill
  • 341
  • 1
  • 3
  • 9