Enumerations in TypeScript

enumeration

Using enumeration, we can define some constants with names. Enumerations can be used to express intentions clearly or to create a distinct set of use cases. TypeScript supports numeric and string based enumeration.

Numeric enumeration

First, let's look at numerical enumeration. If you have used other programming languages, you should be familiar with it.

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}

As above, we have defined a numeric enumeration, and the Up is initialized to 1. The remaining members will automatically grow from 1. In other words, the value of Direction.Up is 1, Down is 2, Left is 3, and Right is 4.

We can also do without initializers at all:

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

Now, the value of Up is 0, the value of Down is 1, and so on. This self growing behavior is useful when we don't care about the value of members, but note that the value of each enumerated member is different.

Enumerations are simple: access enumerating members through enumerating properties, and access enumerating types by enumerating names:

enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Princess Caroline", Response.Yes)

Numeric enumerations can be mixed into calculated and constant members (as shown below). In short, enumerations without initializers are either placed first or after enumerations initialized with numeric constants or other constants. In other words, the following situations are not allowed:

enum E {
    A = getSomeValue(),
    B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}

String Enum

The concept of string enumeration is simple, but there are subtle runtime differences. In a string enumeration, each member must be initialized with a string literal or another string enumeration member.

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

Since string enumeration has no self growth behavior, string enumeration can be serialized well. In other words, if you are debugging and have to read the runtime value of a numeric enumeration, this value is usually difficult to read - it does not express useful information (although reverse mapping will help). String enumeration allows you to provide a meaningful and readable value at runtime, independent of the name of the enumeration member.

Heterogeneous enums

From a technical point of view, enumeration can mix string and numeric members, but it seems that you won't do so:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

Unless you really want to take advantage of JavaScript runtime behavior, we don't recommend doing so.

Calculated and constant members

Each enumeration member takes a value, which can be constant or calculated. Enumeration members are treated as constants when the following conditions are met:

It is the first member of the enumeration and has no initializer. In this case, it is given the value 0:

// E.X is constant:
enum E { X }

It does not have an initializer and its previous enumeration member is a numeric constant. In this case, the value of the current enumeration member is the value of its previous enumeration member plus 1.

// All enum members in 'E1' and 'E2' are constant.

enum E1 { X, Y, Z }

enum E2 {
    A = 1, B, C
}

Enumeration members are initialized with constant enumeration expressions. Constant enumeration expressions are a subset of TypeScript expressions that can be evaluated at compile time. When an expression satisfies one of the following conditions, it is a constant enumeration expression:

  • An enumeration expression literal (mainly string literal or numeric literal)
  • A reference to a previously defined constant enumeration member (which can be defined in different enumeration types)
  • Parenthesized constant enumeration expression
  • One of the unary operators +, -, ~ is applied to a constant enumeration expression
  • Constant enumeration expressions are the operands of binary operators +, -, *, /,%, <, > >, > >, &, |, ^. If the constant enumeration expression evaluates to NaN or Infinity, an error will be reported at the compilation stage.

Enumeration members in all other cases are treated as values that need to be calculated.

enum FileAccess {
    // constant members
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // computed member
    G = "123".length
}

Union enumeration and types of enumeration members

There is a special subset of non computed constant enumeration members: literal enumeration members. Literal enumeration members are constant enumeration members without initial values, or values are initialized to

  • Any string literal (for example: "foo", "bar", "baz")
  • Any numeric literal (e.g. 1, 100)
  • Numeric literal to which a unary sign is applied (for example: - 1, - 100)

When all enumeration members have literal enumeration values, it has a special semantics.

First, enumeration members become types! For example, we can say that some members can only be the values of enumeration members:

enum ShapeKind {
    Circle,
    Square,
}

interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}

interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}

let c: Circle = {
    kind: ShapeKind.Square,
    //    ~~~~~~~~~~~~~~~~ Error!
    radius: 100,
}

Another change is that the enumeration type itself becomes a union of each enumeration member. Although we haven't discussed [union types] (. / advanced types. Md#union types), as long as you know that through union enumeration, the type system can take advantage of the fact that it can know the collection of values in enumeration. Therefore, TypeScript can catch stupid mistakes made when comparing values. For example:

enum E {
    Foo,
    Bar,
}

function f(x: E) {
    if (x !== E.Foo || x !== E.Bar) {
        //             ~~~~~~~~~~~
        // Error! Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'.
    }
}

In this example, we first check whether x is not E. foo. If this check is passed, then 𞓜 will have a short circuit effect, and the contents in the if statement body will be executed. However, if this check fails, then x can only be E.Foo, so there is no reason to check whether it is E.Bar.

Runtime enumeration

Enumerations are objects that really exist at run time. For example, the following enumeration:

enum E {
    X, Y, Z
}

Interfaces can be passed to functions:

function f(obj: { X: number }) {
    return obj.X;
}

// Works, since 'E' has a property named 'X' which is a number.
f(E);

Reverse mapping

In addition to creating an object with the attribute name as the object member, the numeric enumeration member also has a reverse mapping from the enumeration value to the enumeration name. For example, in the following example:

enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

TypeScript may compile this code into the following JavaScript:

var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"

In the generated code, the enumeration type is compiled into an object, which contains forward mapping (name - > value) and reverse mapping (value - > name). Reference enumeration members are always generated to access properties and never inline code.

Note that no reverse mapping is generated for string enumeration members.

const enumeration

In most cases, enumeration is a very effective scheme. However, in some cases, the demand is very strict. To avoid the overhead of extra generated code and extra indirect access to enumeration members, we can use const enumeration. Constant enumeration is defined by using the const modifier on the enumeration.

const enum Enum {
    A = 1,
    B = A * 2
}

Constant enumerations can only use constant enumeration expressions, and unlike regular enumerations, they are deleted at compile time. Constant enumeration members are inlined where they are used. This is possible because constant enumerations are not allowed to contain calculated members.

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]

The generated code is:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

External enumeration

An external enumeration is used to describe the shape of an existing enumeration type.

declare enum Enum {
    A = 1,
    B,
    C = 2
}

There is an important difference between external enumeration and non external enumeration. In normal enumeration, members without initialization methods are treated as constant members. For external enumerations of non constant numbers, when there is no initialization method, it is regarded as one that needs to be evaluated.

Tags: Java C# TypeScript

Posted on Thu, 14 Oct 2021 16:46:27 -0400 by poscribes