Using Enums in TypeScript

Xiwen Tu

Xiwen Tu

Author, Full-stack Engineer
Yifeng Wang

Yifeng Wang

Editor, Head of Graphics Architecture
Fanjing Zhang

Fanjing Zhang

Designer, Head of Product Design
Thumbnail

Prelude

This article is not intended to teach you how to use enums in Typescript, but rather to discuss the problems of using them in real-world scenarios, based on our experiences and some references.

Note that link marks a hyperlink to TypeScript Playground for checking out examples.

Introducing Enums

Enums are provided by TypeScript to define constants with names that clearly express intent, or to create a set of distinguished cases.

The value and type of an enum are one and the same, and the type of an enum member is a subtype of that enum type. There are two scenarios to be aware of when defining enums.

  • When all enum members are literal enum values, all of these members are both values and types.
  • If there are non-literal members in the enum, then all members of that enum can only be used as values.
/* enum members are all literal values */
enum Day {
  Monday,
  Tuesday,
  Wednesday,
};

const monday: Day = Day.Monday; // valid ๐Ÿ˜Ž
const thusday: Day.Tuesday = Day.Tuesday; // valid ๐Ÿ˜Ž


/* enum members contain non-literal enum values */
enum NextDay {
  Monday,
  Tuesday,
  Wednesday = Day.Wednesday, /* non-literal enum values */
};

const nextMonday: NextDay = NextDay.Monday; // valid ๐Ÿ˜Ž
const nextThusday: NextDay.Tuesday = NextDay.Tuesday; // invalid ๐Ÿ˜“
const nextWednesday: NextDay.Wednesday = NextDay.Wednesday; // invalid ๐Ÿ˜“

You can see that Day and NextDay are compiled into JavaScript with exactly the same structure (link).

var Day;
(function (Day) {
  Day[Day["Monday"] = 0] = "Monday";
  Day[Day["Tuesday"] = 1] = "Tuesday";
  Day[Day["Wednesday"] = 2] = "Wednesday";
})(Day || (Day = {}));

var NextDay;
(function (NextDay) {
  NextDay[NextDay["Monday"] = 0] = "Monday";
  NextDay[NextDay["Tuesday"] = 1] = "Tuesday";
  NextDay[NextDay["Wednesday"] = 2] = "Wednesday";
})(NextDay || (NextDay = {}));

Fundamentals of Enums

Firstly in JavaScript, we generally tend to use objects for defining constant configurations. But in TypeScript, defining these constants with enums can be more terse and expressive.

/* JS: Constants Config */
const SERVICE_STATUS = {
  SUCCESS: 200,
  NOT_FOUND: 404,
  UNKONW_ERROR: 500,
};

/* TS: Enum Config */
enum SERVICE_STATUS {
  SUCCESS = 200,
  NOT_FOUND = 404,
  UNKONW_ERROR = 500,
};

Then, different definitions of enums leads to different behaviors and different compiled JavaScript output.

For example, here is an enum definition (link).

enum Day {
  Monday = 'monday',
  Tuesday = 'tuesday',
  Wednesday = 'wednesday',
};

const day: Day = Day.Monday;

Its corresponding JavaScript compiled output looks like this.

var Day;
(function (Day) {
  Day["Monday"] = "monday";
  Day["Tuesday"] = "tuesday";
  Day["Wednesday"] = "wednesday";
})(Day || (Day = {}));

const day = Day.Monday;

So we can see in JavaScript, enums are converted to objects, and two-way mappings are added inside the objects, which undoubtedly increases size of the bundle.

As a comparison, we can take a look at the result of using theconst enum definition (link).

const enum Day {
  Monday = 'monday',
  Tuesday = 'tuesday',
  Wednesday = 'wednesday',
};

const day: Day = Day.Monday;

When the above code is compiled into JavaScript in strict mode, the constant enum definition is removed and its members are replaced with corresponding inlined values.

"use strict";

const day = "monday" /* Day.Monday */;

So itโ€™s clear that enums defined by const enum in TypeScript are more likely to be used as values (inferred from the compiled JavaScript results). Whereas enums defined with enum can be used as both values and types.

Last thing to note is that two enums cannot be assigned to each other, even if their members are identical (link).

enum Day {
  Monday,
  Tuesday,
  Wednesday,
};

enum NextDay {
  Monday,
  Tuesday,
  Wednesday,
};

const day: Day = Day.Monday; // valid ๐Ÿ˜Ž

const nextMonday: Day.Monday = NextDay.Monday; // invalid ๐Ÿ˜ญ
const nextTuesday: NextDay.Tuesday = NextDay.Tuesday; // valid ๐Ÿ˜Ž

Caveats

Using enums as types can lead to some confusing behavior.

Firstly, in the world of structural typing, enums sticks to nominal typing. This means that even if one value is valid and compatible, it canโ€™t be passed to a function or object that requires a string enum (link).

enum Direction {
  Up = 'up',
  Down = 'down',
  Left = 'left',
  Right = 'right',
}

declare function logDirection(direction: Direction): void;

logDirection('up'); // invalid ๐Ÿค”

logDirection(Direction.Up); // valid

What if we replace it with a JavaScript constant object (link)?

const Direction = {
  Up: 'up',
  Down: 'down',
  Left: 'left',
  Right: 'right',
} as const;

type ValueOf<T> = T[keyof T];

declare function logDirection(direction: ValueOf<typeof Direction>): void;

logDirection('up'); // valid ๐Ÿ˜„

logDirection(Direction.Up); // valid

If the value of one enum member is a numeric literal, then the type of that enum will be widen to number (link).

enum SERVICE_STATUS {
  SUCCESS = 200,
  NOT_FOUND = 404,
  UNKONW_ERROR = 500,
};

const getCode = (code: SERVICE_STATUS) => code;

getCode(200); // valid

getCode(123); // valid ๐Ÿ˜ฒ

What if we replace it with a JavaScript constant object (link)?

const SERVICE_STATUS = {
  SUCCESS: 200,
  NOT_FOUND: 404,
  UNKONW_ERROR: 500, 
} as const;

type ValueOf<T> = T[keyof T]; 

const getCode = (code: ValueOf<typeof SERVICE_STATUS>) => code;
getCode(200); // valid

getCode(123); // invalid ๐Ÿ˜„

Heterogeneous enums (enum whose member types are different) can lead to weird behavior (link).

enum Direction {
  Up,
  Down,
  Left,
  Right = 'right',
};

const logDirection = (direction: Direction) => direction;

logDirection(Direction.Up); // valid
logDirection(Direction.Right); // valid
logDirection('right'); // invalid ๐Ÿ˜ฎ
logDirection(100); // valid ๐Ÿ˜ฑ

What if we replace it with a JavaScript constant object (link)?

const Direction = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 'right',
} as const;

type ValueOf<T> = T[keyof T]; 

const logDirection = (direction: ValueOf<typeof Direction>) => direction;

logDirection(Direction.Up); // valid
logDirection(Direction.Right); // valid
logDirection('right'); // valid ๐Ÿ˜Ž
logDirection(100); // invalid ๐Ÿ˜ฑ

Then, enums used in TypeScript canโ€™t be tree shaked when compiled into JavaScript, because itโ€™s compiled into IIFE.

To be clear, enums can be used to define some constants with names. But if the constants are required to be objects (e.g. complex configuration items), enums are obviously not sufficient to handle them (link).

/* sorting rules */
const Collation = {
  ASC: {
    key: 'ASC',
    value: 1,
  },
  DESC: {
    key: 'DESC',
    value: 2,
  },
} as const;

/* define types */
type ValueOf<T> = T[keyof T];

/* use it as configurations */
type Collation = ValueOf<typeof Collation>;

const collationOptions: Collation[] = Object.values(Collation);

/* use it as default values */
type CollationValue = (ValueOf<typeof Collation>)['value'];

const currentValue: CollationValue = Collation.ASC.value;

Takeaways

TypeScript has many advantages over JavaScript:

  1. Interface-oriented development brings great extensibility.
  2. Static type checking can help developers write more robust code, significantly improving code quality and comprehensibility. Types are one of the best forms of documentation.
  3. Code integrity and intelligent awareness, which can be one of the biggest advantages.

TypeScript enums can clearly define simple configuration, its existence is reasonable, but not necessarily the most appropriate. The overall optimal solution is not always also the local optimal solution.

Based on the above analysis, we also see many defects (or designed features) of TypeScript enums, even when the source code is not compiled into JavaScript. In practice, then, it is possible to choose the best - using constant objects instead of enums when necessary. What we propose can make it possible to:

  • Making types more rigorous and reliable.
  • Allowing complex configuration items to complete the loop in daily use.
Copyright ยฉ 2022 Affine. All rights reserved.
Made by Affine