Welcome guys who are interested to “relearn TS” (scan the QR code at the end of this article).

First, basic knowledge

Boolean variables in JavaScript have a limited range of values, namely true and false. With enumerations in TypeScript, you can customize similar types.

1.1 Enumeration

Here’s a simple example of an enumeration:

enum NoYes {
  No,
  Yes,
}
Copy the code

No and Yes are called members of the enumerated NoYes. As with object literals, trailing commas are allowed. The NoYes enumeration allows easy access to its members, such as:

function toChinese(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'no';
    case NoYes.Yes:
      return 'is';
  }
}

assert.equal(toChinese(NoYes.No), 'no');
assert.equal(toChinese(NoYes.Yes), 'is');
Copy the code

1.1.1 Enumeration member values

Each enumerator has a name and a value. The default type of a numeric enumerator value is number. That is, the value of each member is a number:

enum NoYes {
  No,
  Yes,
}

assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);
Copy the code

Instead of having TypeScript specify the values of enumeration members for us, we can assign them manually:

enum NoYes {
  No = 0,
  Yes = 1,}Copy the code

This explicit assignment via the equals sign is called initializer. If an enumeration member’s value is explicitly assigned, but no subsequent member’s value is shown, TypeScript increments the value of subsequent members based on the current member’s value, such as C in the following Enum Enum:

enum Enum {
  A,
  B,
  C = 4,
  D,
  E = 8,
  F,
}

assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0.1.4.5.8.9]);Copy the code

1.2 Conversion of enumerator names

There are several conventions for naming constants:

  • Traditionally, JavaScript uses all uppercase names, which is a convention it inherits from Java and C:Number.MAX_VALUE;
  • Symbols, as they are known, are represented in camel form and begin with a lowercase letter because they are associated with attribute names:Symbol.asyncIterator;
  • TypeScript manuals use humped names that begin with a capital letter. This is the standard TypeScript style that we use forNoYesEnumeration.

1.3 Reference enumerator names

Similar to JavaScript objects, we can use square brackets to refer to enumerators that contain illegal characters:

enum HttpRequestField {
  'Accept'.'Accept-Charset'.'Accept-Datetime'.'Accept-Encoding'.'Accept-Language',
}

assert.equal(HttpRequestField['Accept-Charset'].1);
Copy the code

1.4 String-based enumeration

In addition to numeric enumerations, we can also use strings as enumerator values:

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');
Copy the code

For pure string enumerations, we cannot omit any initializer.

1.5 Heterogeneous Enumeration

The last type of enumeration is called heterogeneous enumeration. Heterogeneous enumerations have member values that are a mixture of numbers and strings:

enum Enum {
  A,
  B,
  C = 'C',
  D = 'D',
  E = 8,
  F,
}

assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0.1.'C'.'D'.8.9]);Copy the code

Note that the previous rule also applies here: if the previous member value is a number, we can omit the initializer. Heterogeneous enumeration is rarely used because of its few applications.

Currently TypeScript only supports numbers and strings as enumeration member values. Other values, such as Symbols, are not allowed.

2. Specify enumerator values

TypeScript distinguishes three ways to specify enumeration member values:

  • Initialize with a literal:
    • Implicitly specified;
    • By number literals or string literals.
  • A constant enumerator is initialized with an expression that evaluates its result at compile time.
  • Evaluated enumerators can be initialized with any expression.

2.1 Literal enumerators

If the enumeration has only literal members, we can use those members as types (similar to the way numeric literals can be used as types) :

enum NoYes {
  No = 'No',
  Yes = 'Yes',}function func(x: NoYes.No) {
  return x;
}

func(NoYes.No); // OK

//@ts-ignore: Argument of type '"No"' is not assignable to
// parameter of type 'NoYes.No'.
func('No');

//@ts-ignore: Argument of type 'NoYes.Yes' is not assignable to
// parameter of type 'NoYes.No'.
func(NoYes.Yes);
Copy the code

In addition, literal enumerations support integrity checking (which we’ll cover later).

TypeScript 2.6 supports ignoring errors in.ts files by using // @ts-ignore at the top of an error line.

// the @ts-ignore comment ignores all errors in the next line. It is recommended in practice to add a hint after @ts-ignore explaining what errors were ignored.

Please note that this comment only hides errors, and we recommend that you use this comment sparingly.

2.2 Const enumerator

An enumerator is a constant if its value can be evaluated at compile time. Therefore, we can specify its value implicitly (that is, let TypeScript specify its value for us). Or we can specify its value explicitly and allow only the following syntax:

  • A numeric literal or a string literal
  • A reference to a previously defined constant enumerator
  • parentheses
  • Unary operator+.-.~
  • Binary operator+.-.*./.%.<<.>>.>>>.&.|.^

Here is an example of an enumeration whose members are constants:

enum Perm {
  UserRead     = 1 << 8,
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,}Copy the code

If the enumeration contains only constant members, members can no longer be used as types. But we can still do integrity checks.

2.3 Counting enumerators

You can set the value of an enumerator through any expression. Such as:

enum NoYesNum {
  No = 123,
  Yes = Math.random(), // OK
}
Copy the code

This is an enumeration of numbers. String enumerations and heterogeneous enumerations are more restrictive. For example, we can’t call some method to set the value of an enumeration member:

enum NoYesStr {
  No = 'No'.//@ts-ignore: Computed values are not permitted in
  // an enum with string valued members.
  Yes = ['Y'.'e'.'s'].join(' '),}Copy the code

Third, the disadvantages of numerical enumeration

3.1 Disadvantages: Log output

When we print members of a numeric enumeration, we see only numbers:

enum NoYes { No, Yes }

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
/ / 0
/ / 1
Copy the code

3.2 Disadvantages: loose inspection

When enumerations are used as types, more than the enumerator’s value is allowed – any number can be accepted:

enum NoYes { No, Yes }
function func(noYes: NoYes) {}

func(33); // no error!
Copy the code

Why aren’t there more stringent static checks? Daniel Rosenwasser explains:

This behavior is caused by bitwise operations. Sometimes SomeFlag. Foo | SomeFlag. The Bar is going to produce another SomeFlag. Instead, you end up with number, and you don’t want to fall back to SomeFlag.

I think if we still have enumerations after we run TypeScript again, we’ll create a separate construct for bitflags.

3.3 Suggestion: Use string enumeration

My suggestion is to use string enumerations:

enum NoYes { No='No', Yes='Yes' }
Copy the code

On the one hand, logging is more human-friendly:

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 'No'
// 'Yes'
Copy the code

On the other hand, we get stricter type checking:

function func(noYes: NoYes) {}

//@ts-ignore: Argument of type '"abc"' is not assignable
// to parameter of type 'NoYes'.
func('abc');

//@ts-ignore: Argument of type '"Yes"' is not assignable
// to parameter of type 'NoYes'.
func('Yes');
Copy the code

Use cases for enumeration

4.1 Use case: bit pattern

In the Node.js file system module, several functions have parameter modes. Its value is used to specify file permissions through uniX-reserved encodings:

  • Permissions are specified for three types of users:
    • User: The owner of the file
    • Group: Members of the group associated with a file
    • All: All of them
  • For each category, the following permissions can be granted:
    • R (read) : allows users in the category to read files
    • W (write) : Allows users in the category to change files
    • X (execute) : Allows users in the category to execute files

This means that permissions can be represented as 9 bits (3 categories, each with 3 permissions) :

The user group all
permissions R, W, x R, W, x R, W, x
position Eight, seven, six 5 4 3 2 1 0

Although this is not done in Node.js, we can use an enumeration to handle these flags:

enum Perm {
  UserRead     = 1 << 8, 
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,}Copy the code

Bitmode by bitwise OR (OR) combination:

// User can change, read and execute; everyone else can only read and execute
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.UserExecute |
  Perm.GroupRead | Perm.GroupExecute |
  Perm.AllRead | Perm.AllExecute,
  0o755);

// User can read and write; group members can read; everyone can’t access at all.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.GroupRead,
  0o640);
Copy the code

Octal, abbreviated OCT or O, a counting system based on eight digits, 0,1,2,3,4,5,6,7, carried by one of the eight digits. Octal 0o755 corresponds to the decimal value 493.

4.1.1 Substitution of counterpoint mode

The main idea behind the bit pattern is that there is a set of flags, and any subset of these flags can be selected. Thus, using Set to select a subset is a more descriptive way of performing the same task:

enum Perm {
  UserRead,
  UserWrite,
  UserExecute,
  GroupRead,
  GroupWrite,
  GroupExecute,
  AllRead,
  AllWrite,
  AllExecute,
}

function writeFileSync(
  thePath: string, permissions: Set<Perm>, content: string) {
  / /...
}

writeFileSync(
  '/tmp/hello.txt'.new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
  'Hello! ');
Copy the code

4.2 Use Cases: Multiple constants

Sometimes we have a set of constants of the same type:

// Log level:
const off = Symbol('off');
const info = Symbol('info');
const warn = Symbol('warn');
const error = Symbol('error');
Copy the code

This is a good enumeration use case:

enum LogLevel {
  off = 'off',
  info = 'info',
  warn = 'warn',
  error = 'error',}Copy the code

The benefits of this enumeration are:

  • Constant names are grouped and nested in a namespaceLogLevelInside.
  • LogLevelWhenever one of these constants is needed, you can use a type, and TypeScript does static checking.

4.3 Use Cases: more self-descriptive than Boolean values

When Boolean values are used to represent alternatives, enumerations are usually a more self-descriptive choice.

4.3.1 Boolean examples: Ordered and unordered lists

For example, to indicate whether a list is ordered, we can use a Boolean value:

class List1 {
  isOrdered: boolean;
  / /...
}
Copy the code

However, enumerations are more self-descriptive and have the additional benefit that we can add more options later if needed.

enum ListKind { ordered, unordered }

class List2 {
  listKind: ListKind;
  / /...
}
Copy the code
4.3.2 Boolean Example: Failure and Success

Similarly, we can indicate whether an operation succeeded or failed by Boolean or enumeration:

class Result1 {
  success: boolean;
  / /...
}

enum ResultStatus { failure, success }

class Result2 {
  status: ResultStatus;
  / /...
}
Copy the code

4.4 Use case: Safer string constants

Consider the following functions for creating regular expressions.

const GLOBAL = 'g';
const NOT_GLOBAL = ' ';

type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;

function createRegExp(source: string,
  globalness: Globalness = NOT_GLOBAL) {
    return new RegExp(source, 'u' + globalness);
}

assert.deepEqual(
  createRegExp('abc', GLOBAL),
  /abc/ug);
Copy the code

It is more convenient to use string-based enumeration:

enum Globalness {
  Global = 'g',
  notGlobal = ' ',}function createRegExp(source: string, globalness = Globalness.notGlobal) {
  return new RegExp(source, 'u' + globalness);
}

assert.deepEqual(
  createRegExp('abc', Globalness.Global),
  /abc/ug);
Copy the code

5. Runtime enumeration

TypeScript compiles enumerations to JavaScript objects. For example, define the following enumeration:

enum NoYes {
  No,
  Yes,
}
Copy the code

TypeScript compiles this enumeration to:

var NoYes;
(function (NoYes) {
  NoYes[NoYes["No"] = 0] = "No";
  NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));
Copy the code

In this code, the following assignment is done:

NoYes["No"] = 0;
NoYes["Yes"] = 1;

NoYes[0] = "No";
NoYes[1] = "Yes";
Copy the code

There are two sets of assignment operations:

  • The first two assignment statements map enumerator names to values.
  • The last two assignment statements map values to names. This is called reverse mapping, which we’ll cover later.

5.1 Reverse Mapping

Given a numeric enumeration:

enum NoYes {
  No,
  Yes,
}
Copy the code

The normal mapping is from the member name to the member value:

// Static lookup
assert.equal(NoYes.Yes, 1);

// Dynamic lookup
assert.equal(NoYes['Yes'].1);
Copy the code

Numeric enumerations also support reverse mapping from member values to member names:

assert.equal(NoYes[1].'Yes');
Copy the code

5.2 String based enumeration at runtime

String-based enumerations have a simpler representation at run time.

Consider the following enumeration:

enum NoYes {
  No = 'NO! ',
  Yes = 'YES! ',}Copy the code

It compiles to the following JavaScript code:

var NoYes;
(function (NoYes) {
    NoYes["No"] = "NO!";
    NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));
Copy the code

TypeScript does not support reverse mapping based on string enumerations.

Const enumeration

If the enumeration is prefixed with the const keyword, there is no representation at run time and the value of the member is used directly.

6.1 Compiling nonconst enumerations

First let’s look at nonconst enumerations:

enum NoYes {
  No,
  Yes,
}

function toChinese(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'no';
    case NoYes.Yes:
      return 'is'; }}Copy the code

TypeScript compiles this code to:

var NoYes;
(function (NoYes) {
    NoYes[NoYes["No"] = 0] = "No";
    NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));
function toChinese(value) {
    switch (value) {
        case NoYes.No:
            return 'no';
        case NoYes.Yes:
            return 'is'; }}Copy the code

6.2 Compiling const enumerations

This is basically the same as the previous code, but with the const keyword:

const enum NoYes {
  No,
  Yes,
}

function toChinese(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'no';
    case NoYes.Yes:
      return 'is'; }}Copy the code

Now, the previously generated NoYes object is gone, leaving only the values of its members:

function toChinese(value) {
    switch (value) {
        case 0 /* No */:
            return 'no';
        case 1 /* Yes */:
            return 'is'; }}Copy the code

Enumeration at compile time

7.1 Enumerations are objects

TypeScript treats (nonconst) enumerations as objects:

enum NoYes {
  No = 'No',
  Yes = 'Yes',}function func(obj: { No: string }) {
  return obj.No;
}

assert.equal(
  func(NoYes),
  'No');
Copy the code

7.2 Literal enumeration comprehensiveness check

When we accept an enumerator value, we usually make sure that:

  • We do not receive illegal values;
  • We are not missing any enumerator values. (This is especially important if new enumerators are added later.)
7.2.1 Defense against Invalid Values

In the following code, we take two actions against invalid values:

enum NoYes {
  No = 'No',
  Yes = 'Yes',}function toChinese(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'no';
    case NoYes.Yes:
      return 'is';
    default:
      throw new TypeError('Unsupported value: ' + JSON.stringify(value));
  }
}

assert.throws(
  //@ts-ignore: Argument of type '"Maybe"' is not assignable to
  // parameter of type 'NoYes'.
  () => toChinese('Maybe'),
  /^TypeError: Unsupported value: "Maybe"$/);
Copy the code

These measures are:

  • At compile time, this typeNoYesPrevents invalid values from being passed tovalueParameters;
  • At run time, if it contains other valuesdefaultThe branch throws an exception.
7.2.2 Comprehensive check against missing scenarios

We can take another step. The following code does a comprehensive check: TypeScript warns us if we forget to consider all enumerators.

enum NoYes {
  No = 'No',
  Yes = 'Yes',}function throwUnsupportedValue(value: never) :never {
  throw new TypeError('Unsupported value: ' + value);
}

function toChinese2(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'no';
    case NoYes.Yes:
      return 'is';
    default: throwUnsupportedValue(value); }}Copy the code

How does a comprehensive inspection work? In each case, TypeScript infers the type of value:

function toChinese2b(value: NoYes) {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return 'no';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return 'is';
    default:
      constz: never = value; throwUnsupportedValue(value); }}Copy the code

In the default branch, TypeScript infer that value is of type never. However, if we add a Maybe member to the NoYes enumeration and then value is inferred to be of type noyes.maybe, the type of the variable is statically incompatible with the type of the parameter in the throwUnsupportedValue() method. Therefore, we receive the following error message when compiling:

Argument of type ‘NoYes.Maybe’ is not assignable to parameter of type ‘never’.

Fortunately, this kind of comprehensiveness also applies to the following if statements:

function toChinese3(value: NoYes) {
  if (value === NoYes.No) {
    return 'no';
  } else if (value === NoYes.Yes) {
    return 'is';
  } else{ throwUnsupportedValue(value); }}Copy the code
7.2.3 Another method of comprehensive examination

Alternatively, if we specify a return type for the following toChinese() function, we can also achieve a comprehensive check:

enum NoYes {
  No = 'No',
  Yes = 'Yes',}function toChinese(value: NoYes) :string {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return 'no';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return 'is'; }}Copy the code

If we add members to NoYes, TypeScript warns that the toChinese() method may return undefined.

Disadvantages of this approach: This approach does not work with if statements.

7.3 Keyof and Enumeration

We can use the keyof type operator to create a type whose element is the keyof an enumeration member. When we do this, we need to use both keyof and Typeof together:

enum HttpRequestKeyEnum {
  'Accept'.'Accept-Charset'.'Accept-Datetime'.'Accept-Encoding'.'Accept-Language',}type HttpRequestKey = keyof typeof HttpRequestKeyEnum;
  // = 'Accept' | 'Accept-Charset' | 'Accept-Datetime' |
  // 'Accept-Encoding' | 'Accept-Language'

function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
  / /...
}
Copy the code

Why is that? This is more convenient than defining the HttpRequestKey type directly.

7.3.1 Using Keyof Instead of Typeof

If you use keyof instead of Typeof, you get another, less useful type:

type Keys = keyof HttpRequestKeyEnum;
  // = 'toString' | 'toFixed' | 'toExponential' |
  // 'toPrecision' | 'valueOf' | 'toLocaleString'
Copy the code

The result of keyof HttpRequestKeyEnum is the same as that of keyof Number.

This article mainly refers to “German Ruan Yifen” — The god Axel Rauschmayer’s numeric-enums article, if you are interested in reading the original article. 2 ality.com/2020/01/typ…

Viii. Reference resources

  • typescript-enums