11

Please have a look at this simple code:

const enum MyEnum {
    Zero
} 

const foo: MyEnum.Zero = 0 // OK as expected (since MyEnum.Zero is zero)
const bar: MyEnum.Zero = 1 // OK, but expected Error! Why?

How can I enforce exact, narrow number type, i.e. 0 in this case?

Playground

enums seem to be broken https://github.com/microsoft/TypeScript/issues/11559

3
  • 1
    Looks like you answered your own question... numeric enums are also used as bit flags, and there is no way to ask for a "non-flag" numeric enum. You can make your own enum-like object which is stricter, but it's verbose. Do you want me to elaborate? Commented Aug 2, 2019 at 23:50
  • @jcalz Well if it's not hard for you.. But mostly I've decided to go with verbose types like you said Commented Aug 2, 2019 at 23:56
  • @jcalz that verbose enum works like magic BTW. I am appreciating it more and more as I work with it.. Thank you so much! Commented Aug 3, 2019 at 0:21

2 Answers 2

10

It is not really mentioned in the enum section of the TypeScript handbook, but number values are assignable to any numeric enum type. Another draft of a 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.

And currently it is mentioned in a section on type compatibility that numeric enums and number are mutually assignable.

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 (see comment on microsoft/TypeScript#8020). And the maintainers of the language don't seem particularly inclined to try (see microsoft/TypeScript#22311).

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

UPDATE: TypeScript 5.0 introduced an overhaul of enums as implemented in microsoft/TypeScript#50528. Since then, all enums are unions of their members. So if you try to assign a numeric literal to a numeric enum that is known not to contain that value, you'll get an error. You can still assign just number to it though. So in the above, const yellow: Color = 0xFFFF00 is now an error because 0xFFFF00 is a numeric literal not appearing in the enum, but const yellow: Color = 0xFF0000 | 0x00FF00 is still fine because the latter expression has type 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.

Link to code

Sign up to request clarification or add additional context in comments.

9 Comments

Great answer as always, but shouldn't it look more like export const Zero = 0 as const; because currently MyEnum.Zero when used as value has type number instead of 0? In my case this matters, maybe other cases will not be influenced by this..
const z = 0 should infer a type of 0 for z, not number. That is, there's no difference between const z = 0 and const z = 0 as const. Can you demonstrate the issue you're describing? I can't reproduce it.
Well I lost the case, sorry, but it was related to the fact that foo in let foo = MyEnum.Zero will be 0 and number with and without as const respectively.. But, let variable should be number anyway, so... Okay, ignore my comment, I think it's pointless (sorry)
Some strange things: with enum A { A1 = 1}, this assignment runs well: const a:A = 1;, but const a1:A = 2; will cause a ts error: Type '2' is not assignable to type 'A'. This phenomenon seems to conflict with what you mentioned, "all number values ​​are assignable to any numeric enum type"
@肉蛋充肌 Thanks, I've updated the answer to reflect enum changes as of TS5.0.
|
1

As @jcalz has mentioned, Typescript doesn't differentiate between number and MyEnum.X in the case of a Numeric Enum:

any number is assignable to a numeric enum, but this is not true for string enums

And that's sad...

It basically means that you cannot rely on the "type" of MyEnum.Zero by itself at all. So you have two choices:

Suboptimal Solution 1) Use a mutated version of the type

enum MyEnum {
    three = 3,
    four,
    five,
}

type EnumKeysToTrue = { [ P in MyEnum]: true }; // { 3: true; 4: true; 5: true; }
type TrueObject<T extends MyEnum> = Pick<EnumKeysToTrue, T>;

const a0: TrueObject<MyEnum.three> = { 3: true }; // OK, as expected
const a1: TrueObject<MyEnum.three> = { 4: true }; // Error
const a2: TrueObject<3> = { [MyEnum.three]: true }; // OK, as expected, just in case
const a3: TrueObject<4> = { [MyEnum.three]: true }; // Error, as expected, just in case

Playground Link

Here we are actually storing an object with its single key being the enum value that we want and its value being true.

It might not be ideal, as getting the value out of these a{x} variables via Object.keys(a1)[0] might not be preferred; yet, it can work in certain cases.


Suboptimal Solution 2) Use the value of the enum and the type of the RHS of the assignment!

enum MyEnum {
    three = 3,
    four,
    five,
}

type EnumKeysToTrue = { [ P in MyEnum]: true }; // { 3: true; 4: true; 5: true; }
type Enumified<T extends number> = EnumKeysToTrue[T] extends true ? T : never;

const v0: Enumified<MyEnum.three> = MyEnum.four; // Error: Type 'MyEnum.four' is not assignable to type 'MyEnum.three'.ts(2322)
const v1: Enumified<MyEnum.three> = 4;  // Unfortunately, OK. So don't use it this way!
const v2: Enumified<4> = MyEnum.three;  // Type 'MyEnum.three' is not assignable to type '4'
const v3: Enumified<9> = 9;             // error: type 9 is not assignable to never
const v4: Enumified<4> = MyEnum.four;   // OK, as expected :)
let num: number = 9;
const v5: Enumified<typeof num> = MyEnum.four;  // Type 'MyEnum.four' is not assignable to type 'never'.ts(2322)

Playground Link

This one might work much better, since once can take the values of v{x} variables and go from there! Just be careful that the assigned value must be passed to Enumified<> and the Enum must be on the RHS of the assignment, otherwise, like v1 above, it's going to work inadvertently.


PS. You cannot generalize EnumKEyToTrue for all the enums in your codebase. It needs to be one per enum instance.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.