TypeScript: why is exact enum type not enforced?

It is not really mentioned in the enum section of the current TypeScript handbook, but all number values are assignable to any numeric enum type. The current draft of a new TypeScript handbook says the following:

A few of the [assignability] rules are odd because of historical restrictions. For example, any number is assignable to a numeric enum, but this is not true for string enums. Only strings that are known to be part of a string enum are assignable to it. That's because numeric enums existed before union types and literal types, so their rules were originally looser.

It seems that numeric enums in TypeScript have historically been used to support bit fields, using bit masking and bitwise operations to combine explicitly declared enum values to get new ones:

enum Color {
  Red = 0xFF0000,
  Green = 0x00FF00,
  Blue = 0x0000FF
}
const yellow: Color = Color.Red | Color.Green; // 0xFFFF00
const white: Color = Color.Red | Color.Green | Color.Blue; // 0xFFFFFF
const blue: Color = white & ~yellow; // 0x0000FF

And because this use of enums exists in real-world code, it would be a breaking change to alter this behavior. And the maintainers of the language don't seem particularly inclined to try.

So, for better or for worse, numeric enums are loosely typed to be mostly synonymous with number.


It is possible to roll your own stricter enum-like object, but it involves doing by hand a number of the things that happen automatically when you use the enum syntax. Here's one possible implementation (which doesn't give you a reverse mapping):

namespace MyEnum {
  export const Zero = 0;
  export type Zero = typeof Zero;

  export const One = 1;
  export type One = typeof One;

  export const Two = 2;
  export type Two = typeof Two;
}
type MyEnum = typeof MyEnum[keyof typeof MyEnum];

const foo: MyEnum.Zero = 0 // okay
const bar: MyEnum.Zero = 1 // error!

This works as follows... when you write enum X { Y = 123, Z = 456 }, TypeScript introduces a value at runtime named X, with properties X.Y and X.Z. It also introduces types named X, X.Y, and X.Z. The types X.Y and X.Z are just the types of the values X.Y and X.Z. But the type X is not the type of the value X. Instead, it is the union of the property types X.Y | X.Z.

I used namespace, export, const, and type above to achieve a similar effect. But the difference here is that the assignability rule for numeric enums doesn't apply, so you get the strict type checking you expect.

Okay, hope that helps; good luck!

Link to code

Tags:

Typescript