204

Can enum be used as a key type instead of only number or string? Currently it seems like the only possible declaration is x:{[key:number]:any} where key can be of type number or string. Is it possible to make something like in this example:

Example:

enum MyEnum
{
    First,
    Second
}

var layer:{[key:MyEnum]:any};
BuZZ-dEE
  • 6,075
  • 12
  • 66
  • 96
Hivaga
  • 3,738
  • 4
  • 23
  • 36

5 Answers5

347

Since 2018, there is an easier way in Typescript, without using keyof typeof:

let obj: { [key in MyEnum]: any} =
 { [MyEnum.First]: 1, [MyEnum.Second]: 2 };

To not have to include all keys:

let obj: { [key in MyEnum]?: any} =
 { [MyEnum.First]: 1 };

To know the difference between in and keyof typeof, continue reading.


in Enum vs keyof typeof Enum

in Enum compiles to enum values and keyof typeof to enum keys.

With keyof typeof, you cannot change the enum properties:

let obj: { [key in keyof typeof MyEnum]?: any} = { First: 1 };
obj.First = 1;
// Cannot assign to 'First' because it is a read-only property.

... unless you use -readonly:

let obj: { -readonly [key in keyof typeof MyEnum]?: any} = { First: 1 };
obj.First = 1; // works

But you can use any integer key?!:

let obj: { [key in keyof typeof MyEnum]?: any} = { First: 1 };
obj[2] = 1;

keyof typeof will compile to:

{
    [x: number]: any;
    readonly First?: any;
    readonly Second?: any;
}

Note both the [x: number] and the readonly properties. This [x: number] property doesn't exist with a string enum.

But with in Enum, you can change the object:

enum MyEnum {
    First,  // default value of this is 0
    Second, // default value of this is 1
}

let obj: { [key in  MyEnum]?: any} = { [MyEnum.First]: 1 };
obj[MyEnum.First] = 1; // can use the enum...
obj[0] = 1;            // but can also use the enum value, 
                       // as it is a numeric enum by default

It's a numeric enum. But we can't use any number:

obj[42] = 1;
// Element implicitly has an 'any' type because 
// expression of type '42' can't be used to index type '{ 0?: any; 1?: any; }'.
// Property '42' does not exist on type '{ 0?: any; 1?: any; }'.

The declaration compiles to:

{
    0?: any;
    1?: any;
}

We allow only 0 and 1, the values of the enum.

This is in line with how you would expect an enum to work, there are no surprises unlike keyof typeof.

It works with string and heterogenous enums:

enum MyEnum
{
    First = 1,
    Second = "YES"
}

let obj: { [key in  MyEnum]?: any} = { [MyEnum.First]: 1, [MyEnum.Second]: 2 };
obj[1] = 0;
obj["YES"] = 0;

Here the type is:

{
    1?: any;
    YES?: any;
}

Get immutability with readonly:

let obj: { readonly [key in MyEnum]?: any} = { 
    [MyEnum.First]: 1,
};
obj[MyEnum.First] = 2;
// Cannot assign to '1' because it is a read-only property.

... which makes these keys readonly:

{
    readonly 1?: any;
    readonly 2?: any;
}

Summary

in Enum keyof typeof Enum
Compiles to enum values Compiles to enum keys
Does not allow values outside the enum Can allow numeric values outside the enum if you use a numeric enum
Can change the object, immutability opt-in with readonly Can't change enum props without -readonly. Other numeric values outside the enum can be

Use in Enum if possible. If your codebase uses keyof typeof Enum, be aware of these pitfalls.

xy2
  • 6,040
  • 3
  • 14
  • 34
  • 5
    It seems to work great. When using 'typeof', it looks like, a key in my object was created from a key of an enum, not a value of this key. – Aleks Grunwald Feb 22 '21 at 10:50
  • If i try to define an interface with same structure, it doesn't work. Any idea ? https://www.typescriptlang.org/play?#code/KYOwrgtgBAHlDeAoKKoEMA0UBGiC%20iiANsAC7rYDGAJgFwJQDaA1sAJ5QCWIsAugPz00IDnigBeBPkKJupYACcAZmkrAuKtVNRNWHbn0HoRAbmmIgA – Sachin Gupta Jun 01 '21 at 21:10
  • 3
    "To not have to include all keys:" -> Exactly what I was looking for – mTrebusak Aug 10 '21 at 04:46
  • 4
    Keyof typeof is not equilvalent to key in enum, it creates type that literally consist of keys of enum, not values – Sergey Solomatin Dec 09 '21 at 07:56
  • @SachinGupta I ran into the same issue, and it seems that replacing the interface with a type fixes it. `type Layer = { [key in MyEnum]?: any };` instead of `interface Layer { [key in MyEnum]?: any }` – Dávid Leblay Feb 23 '22 at 00:47
139

Yes. Just type

let layer:{[key in keyof typeof MyEnum]: any}

The keyof keyword is available since Typescript 2.1. See the TypeScript documentation for more details. Using only keyof for enums wouldn't work (you'd get the keys of the enum type and not the enum constants), so you have to type keyof typeof.

Sebastian
  • 5,177
  • 4
  • 30
  • 47
  • This seems like exactly what I was looking for. Tnx – Hivaga May 29 '17 at 14:40
  • @Sebastian but in this case the `value` of layer can be { third: 123 } if we assign it this way: layer.third = 123, can't it? Is it possible to define a type of an object, whose keys can *only* be string keys of an enum (in this case `First` and `Second`)? – Eduard Jul 11 '18 at 16:54
  • I don't understand. Why whould you be able to add a property "third"? – Sebastian Jul 12 '18 at 07:12
  • 11
    And if you don't want all the Enum values to be included forcefully, you can do `{[key in keyof typeof MyEnum]?: any}` (the `?` makes absence of some enums, fine.) – Aidin Dec 06 '19 at 10:31
  • I have this working within the same file scope where the enum used for the keys is declared. In other files, though, `ENUM_KEY_1` isn't defined. How do I export/import enum _keys_ so that I can reference the object properties? – nshew13 Oct 30 '20 at 18:01
  • not working with me, giving this error `A computed property name must be of type 'string', 'number', 'symbol', or 'any'` – Mohamed Abu Galala Apr 19 '21 at 12:25
  • 4
    Refer to Hugo Elhaj-Lahsen's answer below. This answer might now in fact be outdated! – Sebastian Apr 19 '21 at 14:41
  • Hugo Elhaj-Lahsen's answer below is indeed better. This one makes all of the properties read only, which is something you might not want. – Dana Jun 30 '21 at 13:32
132

Short Answer:

let layer: Partial<Record<MyEnum, unknown>>;

Long Answer (with details):

I had the same problem. I wanted to have the following piece of code work.

enum MyEnum {
    xxx = "xxx",
    yyy = "yyy",
    zzz = "zzz",
}

type myType = ...; // Fill here

const o: myType = { // keys should be in MyEnum, values: number
   [MyEnum.xxx]: 2,
   "hi": 5, // <--- Error: complain on this one, as "hi" is not in enum
}

o[MyEnum.yyy] = 8; // Allow it to be modified later

Starting from the other answer on this question, I got to:

type myType = {[key in keyof typeof MyEnum]: number};

But it would nag that o is missing "yyy". So I needed to tell it that the object is going to have some of the enum keys, not all of them. So I got to add ? to get:

type myType = {[key in keyof typeof MyEnum]?: number};

It was working fine, until I added the line at the end of the code to modify the object after its first creation. Now it was complaining that the type inherited through keyof has its properties as readonly and I cannot touch them after the first creation! :| In fact, hovering over myType in Typescript Playground, it will be shown as:

type myType = {
    readonly xxx?: number | undefined;
    readonly yyy?: number | undefined;
    readonly zzz?: number | undefined;
}

Now, to remove that unwanted readonly, I found that I can use:

type myType = {-readonly [key in keyof typeof myEnum1]?: number };

Quite ugly, but working!

Until I played with Typescript Utility Types, and found what I wanted!

type myType = Partial<Record<keyof typeof MyEnum, number>>;

:)

Aidin
  • 25,146
  • 8
  • 76
  • 67
  • that looks good, but my problem as that all of your `myType` values must have the same type (in your example `number`). I'd like to be able to do something like: `type myType = { readonly xxx?: number; readonly yyy?: string; }` any idea if that's possible? – SudoPlz Nov 27 '20 at 21:28
  • @SudoPlz I'd suggest opening up a new question for that. The question here is "how to limit the keys to be of an Enum". Yours is different I believe. Yet, as a wild guess, either `Partial` should work for you or `Partial> | { yyy?: string}`, maybe. – Aidin Dec 05 '20 at 10:43
  • Nice one but the problem with this solution is that number can be undefined – walox Jun 04 '21 at 15:47
  • @walox Typescript had a problem distinguishing `undefined` and missing/optional -- see https://github.com/microsoft/TypeScript/issues/13195. It seems like recently (since a month ago! you can pass a flag to catch it: https://github.com/microsoft/TypeScript/pull/43947) – Aidin Jun 05 '21 at 01:45
26

For those who are seeking for a way to get the keys of the enum instead its value, you just need to change from this:

type myType = Partial<Record<MyEnum, number>>;

to that:

type myType = Partial<Record<keyof typeof MyEnum, number>>;
Alecell
  • 568
  • 6
  • 19
6

actually it worked with me without any workaround.

here is some cases:

enum EnumString {
  a="a",
  b="b"
}

enum EnumNum {
  a,
  b
}

type Type = "a" | "b"


let x1:{[k in EnumString]?:any }={a:1} //working
let x2:{[k in EnumNum]?:any }={a:1} //error: Type '{ a: number; }' is not assignable to type '{ 0?: any; 1?: any; }'
let x3:{[k in Type]?:any }={a:1} //working; defining a union type is easer than defining an enum string 

let y1:{[k: EnumString]?:any }={a:1}
let y2:{[k: EnumNum]?:any }={a:1}
let y3:{[k in Type]?:any }={a:1}

let z1:{[k in keyof typeof EnumString]?:any }={a:1}
let z2:{[k in keyof typeof EnumNum]?:any }={a:1}
let z3:{[k: keyof typeof Type]?:any }={a:1}

it is important to note that the case "z1", even it works well, but you cannot modify it later as it is a read only. you can use the keyword -readonly to remove this constraint.

x1.a=2 
x3.a=2
z1.a=2 //error: read only

I created a typescript playground to see issues of different cases yourself

Sh eldeeb
  • 1,589
  • 3
  • 18
  • 41