Categories
TypeScript

Introduction to TypeScript Enums — Const and Ambient Enums

Spread the love

If we want to define constants in JavaScript, we can use the const keyword. With TypeScript, we have another way to define a set of constants called enums. Enums let us define a list of named constants. It’s handy for defining an entity that can take on a few possible values. In this article, we’ll continue from Part 1 and look at union enums and enum member types, how enums are evaluated at run-time, const enums, and ambient enums.

Union Enums and Enum Member Types

A subset of enum members can act as data types of variables and class members in TypeScript. We can use literal enum members, which are enum members with no specific values assigned to them for annotating data types of our variables and class members. If an enum member has no string literal, numeric literal or a numeric literal with a minus sign before it, then we can use them as data types for other members. For example, we can use them as we do in the following code:

enum Fruit {
  Orange,
  Apple,
  Grape
}

interface OrangeInterface {
  kind: Fruit.Orange;
  color: string;
}

interface AppleInterface {
  kind: Fruit.Apple;
  color: string;
}

class Orange implements OrangeInterface {
  kind: Fruit.Orange = Fruit.Orange;
  color: string = 'orange';
}

class Apple implements AppleInterface{
  kind: Fruit.Apple = Fruit.Apple;
  color: string = 'red';
}

let orange: Orange = new Orange();
let Apple: Orange = new Apple();

In the code above, we used our Fruit enum to annotate the type of the kind field in our OrangeInterface and AppleInterface . We set it so that we can only assign Fruit.Orange to the kind field of the OrangeInterface and the class Orange which implements the OrangeInterface . Likewise, we set the kind field of AppleInterface to the type Fruit.Apple so that we can only assign the kind field to the value of the instances of the Apple class. This way, we can use the kind field as a constant field even though we can’t use the const keyword before a class field.

If we log the values of orange and apple above, we get that orange is:

{kind: 0, color: "orange"}

and apple has the value:

{kind: 1, color: "red"}

When we use enums in if statements, the TypeScript compiler will check that if the enum members are used in a valid way. For example, it’ll prevent us from writing expressions that use enums that always evaluate to true or false . For example, if we write:

enum Fruit {
  Orange,
  Apple,
  Grape
}

function f(x: Fruit) {
  if (
   x !== Fruit.Orange ||
   x !== Fruit.Apple ||
   x !== Fruit.Grape
  ) {

  }
}

Then we get the error message “This condition will always return ‘true’ since the types ‘Fruit.Orange’ and ‘Fruit.Apple’ have no overlap.(2367)“ since at least one of them is always true , so the expression:

x !== Fruit.Orange ||
x !== Fruit.Apple ||
x !== Fruit.Grape

will always evaluate to true . This is because if x can only be of type Fruit , and if x isn’t Fruit.Orange , then it’s either Fruit.Apple or Fruit.Grape , so either of them must be true .

This also means that the enum type itself is the union of each member, since each member can be used as a type. If a data type has the enum as the type, then it must always have one of the members in it as the actual type.

How Enums are Evaluated at Runtime

Enums are converted to real objects when they’re compiled by the TypeScript compiler, so they’re always treated like objects at runtime. This means that if we have an enum, then we can use its member names as property names of an enum object when we need to pass it in as a parameter with it. For example, if we have the following code:

enum Fruit {
  Orange,
  Apple,
  Grape
}

function f(fruit: { Orange: number }) {
  return fruit.Orange;
}
console.log(f(Fruit));

Then we get 0 from the console.log output from the code in the last line since we logged the value of fruit.Orange , which is 0 since we didn’t initialize it to any value. Likewise, we can use the same syntax for the destructuring assignment of an enum like we do in the following code:

enum Fruit {
  Orange,
  Apple,
  Grape
}

let { Orange }: { Orange: number } = Fruit;
console.log(Orange);

In the code above, we treat the Orange member inside the Fruit enum as another property of an object, so we can use it to assign it to a new variable with destructuring assignment like we did above. So if we log Orange like we did on the last line of the code snippet above, then we get 0 again. Also, we can use destructuring assignment to assign it to a variable with a name that’s not the same as the property name like we do in the following code:

let { Orange: orange }: { Orange: number } = Fruit;
console.log(orange);

Then we should get 0 again from the console.log statement on the last line of the code above.

Photo by Gary Bendig on Unsplash

Enums at Compile Time

The only exception to the rule that enums are treated as objects if when we use the keyof keyword with enums. The keyof keyword doesn’t work like typical objects. For example if we have:

let fruit: keyof Fruit;

Then the TypeScript compiler expects that we assign strings with number methods to it. For example, if we try to assign something like a 'Orange' to the expression above, we get the following error:

Type '"Orange"' is not assignable to type '"toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"'.(2322)

This isn’t what expect from the typical usage of the keyof keyword since for normal objects, it’s supposed to let us assign the property names of the keys of an object that comes after the keyof keyword. To make TypeScript let us assign 'Orange' , 'Apple' or 'Grape' to it, we can use the typof keyword after the keyof keyword like we do in the following code:

enum Fruit {
  Orange,
  Apple,
  Grape
}

let fruit: keyof typeof Fruit = 'Orange';

The code above would be accepted by the TypeScript compiler and runs because this is what makes TypeScript treats our enum members’ names as key names of an object.

Reverse Mappings

Numeric enums in TypeScript can be mapped from enum values to enum names. We can get an enum member’s name by its value by getting it by the values that are assigned to it. For example, if we have the following enum:

enum Fruit {
  Orange,
  Apple,
  Grape
}

Then we get can the string 'Orange' by getting it by its index like we do with the following code:

console.log(Fruit[0]);

The code above should log 'Orange' since the value of the member Orange is 0 by since we didn’t assign any specific value to it. We can also access it by using the member constant inside the brackets like the following code:

console.log(Fruit[Fruit.Orange]);

Since Fruit.Orange has the value 0, they’re equivalent.

Const Enums

We can add the const keyword before the enum definition to prevent it from being included in the compiled code that’s generated by the TypeScript compiler. This is possible since enums are just JavaScript objects after it’s compiled. For this reason, the values of the enum members can’t be dynamically generated, but they can be computed from other constant values. For example, we can write the following code:

const enum Fruit {
  Orange,
  Apple,
  Grape = Apple + 1
}

let fruits = [
  Fruit.Orange,
  Fruit.Apple,
  Fruit.Grape
]

Then when our code is compiled into ES5, we get:

"use strict";
let fruits = [
    0 /* Orange */,
    1 /* Apple */,
    2 /* Grape */
];

Ambient Enums

To reference an enum that exists somewhere else in the code, we can use the declare keyword before the enum definition to denote that. Ambient enums can’t have values assigned to any members and they won’t be included in compiled code since they’re supposed to reference enums that are defined somewhere else. For example, we can write:

declare enum Fruit {
  Orange,
  Apple,
  Grape
}

If we try to reference an ambient enum that’s not defined anywhere, we’ll get a run-time error since no lookup object is included in the compiled code.

Enum members can act as data types for variables, class members, and any other things that can be typed with TypeScript. An enum itself can also be a data type for these things. Therefore, anything typed with the enum type is a union type of all the member enum types. Enums are included or not depending on what keywords we use before the enum. If they’re defined with const or declare , then they won’t be included in the compiled code. Enums are just objects when converted to JavaScript and the members are converted to properties when compiled to JavaScript. This means that we can use member names as property names of objects in TypeScript.

Leave a Reply

Your email address will not be published. Required fields are marked *