Use Enum as restricted key type in Typescript

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;
}

Auto-increment pitfall

enum MyEnum
{
    First = 1,
    Second,
    Third = 2,
}

let obj: { [key in  MyEnum]?: any} = { 
    [MyEnum.First]: 10, 
    [MyEnum.Second]: 20,
    [MyEnum.Third]: 30,
};
// {1: 10, 2: 30}

The 20 value disappeared! Numeric enums are auto incrementing but allow duplicates. So both the value of MyEnum.Second and MyEnum.Third are 2!

TS does not warn, lint if you can.


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
Enum values can conflict because of auto-increment Enum values can conflict because of auto-increment

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

Leave a Comment