Working with Typescript Enums

Working with Typescript Enums

TypeScript enums are a powerful feature that allows developers to define a set of named constants. However, while enums may seem convenient at first glance, they come with their own set of limitations and drawbacks that can lead to unexpected behavior in certain scenarios. Let's have a look why it is so.

The following is the typescript enum code followed by the compiled javascript code

enum Status {
  Pending,
  Approved,
  Rejected,
}

const currentStatus: Status = Status.Pending;

console.log(currentStatus); // Output: 0
var Status;
(function (Status) {
  Status[Status["Pending"] = 0] = "Pending";
  Status[Status["Approved"] = 1] = "Approved";
  Status[Status["Rejected"] = 2] = "Rejected";
})(Status || (Status = {}));

var currentStatus = Status.Pending;

console.log(currentStatus); // Output: 0

Javascript does a lot behind the scenes to achieve this since enums are not part of it. If you don't explicitly provide string values to the enum, then the value defaults from 0 in increasing sequence

The resultant object will look something like this.

const Status = {
  Pending: 0,
  0 : 'Pending',
  Approved: 1,
  1 : 'Approved',
  Rejected : 2,
  2 : 'Rejected'
}

If we want to get the string value of a Status then we would have to write this syntax which is kind of weird.

console.log(Status[Status.Pending]) // 'Pending'

If you were to show these options in a list eg. a dropdown list we would typically do the following.

const dropdownOptions = Object.keys(Status).map((key) => ({
  label: key,
  value: Status[key],
}));

console.log(dropdownOptions);

This will be the output

 [{
  "label": "0",
  "value": "Pending"
}, {
  "label": "1",
  "value": "Approved"
}, {
  "label": "2",
  "value": "Rejected"
}, {
  "label": "Pending",
  "value": 0
}, {
  "label": "Approved",
  "value": 1
}, {
  "label": "Rejected",
  "value": 2
}]

As we can see, this is not the desired output we were looking for since 0 , 1 , 2 are included in the list. We will have to apply additional filters to the method which is extra work.

Secondly, for enums typescript becomes nominal type system as it cares about the names. Consider the following code where we take enums as a parameter.

function print(status : Status){
 console.log(status);
}

print(Status.Pending) // This will work
print('Pending') 
// This will not work, will show error in underlines 
// 'Argument of type '"Pending"' is not assignable to parameter of type 'Status'.'

Even though the values are the same, typescript will still complain.

Solution 1 (const enum)

We can declare enums using const like this :

const enum Status {
  Pending,
  Approved,
  Rejected,
}

Const enums are a feature in TypeScript that allow you to define enums in such a way that they are completely removed during compilation. Unlike regular enums, which generate JavaScript code at runtime, const enums are replaced with their actual values in the generated JavaScript code. This can lead to more efficient code and potentially better performance.

However it has some limitations :

  1. Numeric Values Only: Const enums can only contain numeric literal values or other constant expressions. This means that string enums and heterogeneous enums (enums with both string and numeric members) cannot be defined as const enums.

  2. No Reverse Mapping: Unlike regular enums, const enums do not support reverse mapping from values to keys. Reverse mapping allows you to retrieve the enum member name from its associated value. Since const enums are completely removed during compilation, this feature is not available.

  3. The TypeScript documentation advises against using const enums in many cases due to their limitations and potential drawbacks. Details in this link
    https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls

Solution 2 (as const)

Instead of defining an enum, as const allows you to create a readonly tuple of string or numeric literals directly. This fixes all the problems we encountered earlier and provides a smoother developer experience.

Here's how you can use as const to define a set of constants:


// Array variant
const Status = ['Pending', 'Approved', 'Rejected'] as const;
type StatusType = typeof Status[number]; // 'Pending' | 'Approved' | 'Rejected'

// OR

// Object variant
const Status = {
  PENDING: 'Pending',
  APPROVED: 'Approved',
  REJECTED: 'Rejected',
} as const;

type ObjectValues<T> = T[keyof T] // Generic type to be used with object enums
type StatusType = ObjectValues<typeof Status> // 'Pending' | 'Approved' | 'Rejected'

// You can use either depending on your needs

In this example:

  • ['Pending', 'Approved', 'Rejected'] creates an array of string literals.

  • as const asserts that the array should be treated as a readonly tuple of string literals.

  • type Status = typeof Status[number] defines a type Status that represents the union of all string literals in the Status tuple, effectively creating a string literal type with the possible values 'Pending', 'Approved', and 'Rejected'.

Using as const in this way provides several advantages over traditional enums:

  1. Immutability: The constants defined using as const are immutable. Once defined, their values cannot be modified or reassigned, ensuring type safety and preventing accidental mutations.

  2. Type Inference: TypeScript infers the type of the constants automatically, eliminating the need for explicit type annotations.

  3. No Runtime Overhead: Unlike enums, which generate runtime code, constants defined using as const are removed entirely during compilation, resulting in cleaner and more efficient JavaScript output.

  4. Flexibility: Constants defined with as const can include any valid TypeScript literals, including string literals, numeric literals, boolean literals, and even object literals. This flexibility allows you to create a wide range of immutable constants tailored to your specific needs.

Overall, as const provides a powerful and flexible alternative to enums for defining immutable constants in TypeScript, offering improved type safety, efficiency, and flexibility in your code.

For more details, please check out Matt Pocock's amazing video on this which helped me get better understanding on enums.

https://www.youtube.com/watch?v=jjMbPt_H3RQ&ab_channel=MattPocock

Thanks for reading! Leave a like or comment if you enjoyed it.