Possible to use enums to restrict values?

Viewed 152

We can easily restrict the supported value using literal types such as below

type GenderConfig = {
  gender: 1 | 0
}

const obj: GenderConfig = {
  gender: 100 // throw error because value is not 1 or 0
}

Now as much as possible I would like to avoid magic number 1 & 0

so I tried to define an enum for that

enum Gender {
  MALE= 1,
  FEMALE=0,
}

type GenderConfig = {
  gender: Gender
}

const obj: GenderConfig = {
  gender: 100 // This doesn't throw error anymore?
}

Playground link

Surprisingly, gender: 100 is no longer throwing error which I don't understand the rationale behind this?

3 Answers

What you've created here is a numeric enum. The generated JavaScript looks like this:

var Gender;
(function (Gender) {
    Gender[Gender["MALE"] = 1] = "MALE";
    Gender[Gender["FEMALE"] = 0] = "FEMALE";
})(Gender || (Gender = {}));

In the console, this object looks like this:

Object
  0: "FEMALE"
  1: "MALE"
  FEMALE: 0
  MALE: 1

Under the hood, the enum is really just a JavaScript object with properties. It has the named properties you defined, and they are assigned a number representing the position in the enum that they exist (FEMALE being 0, MALE being 1), but the object also has number keys with a string value representing the named constant.

Therefore, you can pass in numbers to a function that expects an enum. The enum itself is both a number and a defined constant.

This appears to defeat the type-safe aspect of TypeScript, since you can pass an arbitrary number to a function expecting this enum. There are two reasons why this is useful, though:

  1. When you receive numeric data (say, in a JSON payload from some service), you can convert it to an enum.
  2. Since the enum members have numeric values, you can also use them as bitflags (although since you can set the numeric value for each enum member, as you have done here, this may or may not work well).

Regarding point (1): if you declare an enum without numeric values, e.g. this:

enum Gender {
  MALE = "Male",
  FEMALE = "Female"
}

then you can no longer do this:

const gender: Gender = "Male";

You'll have to do an explicit cast:

const gender = "Male" as Gender;
const MALE = 1;
const FEMALE = 0;

type Gender = typeof MALE | typeof FEMALE;

type GenderConfig = {
  gender: Gender
}

const obj: GenderConfig = {
  gender: 100 // This is now throwing error
}

Playground

Instead of using enums, I've changed it to type and by using the combination of const and typeof, I can avoid magic number and improve readability and consistency, thanks others for the explanation

I do not use enums that much anymore because they are kind of complicated. Maybe this is possible somehow but you can use strings like this instead:

type Gender = 'female' | 'male';
type GenderConfig = {
  gender: Gender
}

Of course you may need to add some mapping if you really need the number representation at some point.

Related