Typescript enums and scope reduction

About enumeration

Reverse mapping principle

In the past, I only knew how to build and use enumerations, but I didn’t know how to get the enumeration content through Typescript. Through keyof typeof

Reverse mapping to obtain enumeration key

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

About constant enumeration

Usage of constant enumeration

  • In most cases, enumerations are meant to give completely valid solutions, however, there are strict requirements to avoid the need to generate additional code when accessing enumeration values. You can use const enumerations, marked as constants.
const enum Enum{
A = 1,
B = A * 2,
}

Constant enumerations can use constant enumeration expressions, which, unlike regular enumerations, are completely deleted during compilation. Constant enumeration members are used inline in the site.

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

let directions = [
  Direction.Up,
  Direction.Down,
  Direction.Left,
  Direction.Right,
];

//The following is the result of the above conversion, displayed inline
"use strict";
let directions = [
    0 /* Direction.Up */,
    1 /* Direction.Down */,
    2 /* Direction.Left */,
    3 /* Direction.Right */,
];

Traps of constant enumeration

》 Inline enumeration values are simple at first, but have some consequences. These gotchas are only related to environment const enums (basically const enums in .d.ts files) and sharing between projects. But if you are publishing or using .d.ts files. You need to consider these pitfalls.

  • The changed mode is fundamentally incompatible with the environment constant enumeration for the reasons listed in the isolatedModules file. This means that if you publish an environment constant enumeration, users will not be able to use isolatedModules and those enumeration values at the same time.

IsolatedModules (isolated module), we generate Typescript code into Jacascript code, which will be executed using other converters such as Babel. However, other converters can only operate on a single file at a time, which means that they cannot be applied depending on Lijie Transcoding for complete type operating systems. This restriction also applies to Typescript’s ts.transileModule API. These restrictions may cause runtime issues with some Typescript features (such as const enumerations and namespaces). Setting the isolate module flag will tell Typescript to warn you if you write a These codes cannot be correctly interpreted by the single file conversion process.

The above content is explained in vernacular as follows:

Why do you need to setisolatedModules to true

Suppose there are two ts files as follows. We exported the Test interface in a.ts and introduced the Test interface in a.ts into b.ts.
Then export Test in b.ts.

// a.ts
export interface Test {}

// b.ts
import { Test } from './a';

export { Test };

What problem will this cause? For example, when Babel escapes ts, it will first delete the type of ts, but when it encounters the b.ts file,

Babel cannot analyze whether export { Test } exports a type or a real js method or variable. At this time, Babel chooses to retain export. However, when the a.ts file is converted, it can be easily determined that it exports a type. When converted to js, the content in a.ts will be cleared, and the Test exported in b.ts is actually from a. Introduced in ts, an error will be reported at this time.

How to solve it?

ts provides import type or export type to clearly identify that what I am importing/exporting is a type, not a variable or method. The type introduced using import type will be deleted by js during conversion.

// b.ts
import { Test } from './a';

export type { Test };
  • You can easily inline values from version A at compile time and import version B at runtime. The enumerations for versions A and B can have different values if you Being very careful led to unexpected flaws, like doing the wrong branch of an if statement. These errors are particularly harmful because often automated tests are run at the same time as building the project, with the same dependency versions, completely ignoring these errors
  • importsNotUsedAsValues:”preserve” Imports of const enumerations used as values will not be ignored, but environmental const enumerations do not guarantee that the .js file exists at runtime. Unresolved imports can cause errors at runtime. Common methods that currently explicitly omit imports, only type imports, const enumeration values are not allowed

How to avoid traps

  • Do not use constant enumeration at all, you can configure eslint [disable const enumeration] (Troubleshooting and FAQ | typescript-eslint Chinese website), obviously, this avoids const enumeration Any problems, but will prevent your project from inlining its own enumeration. Unlike inline enumerations for other projects, there is no problem with inlining a project’s own enumerations. Doesn’t affect performance.
  • Do not publish environment constant enumerations by building with the help of preserveConstEnums. This is the method used internally by the Typescript project itself. preserveConstEnums triggers the same Javascript for const enumerations as normal enumerations. Then, you can safely remove the const modifier from the .d.ts file during the build.

Environment enumeration

Environment enumerations are used to describe the shape of existing enumeration types.

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

An important difference between ambient and non-ambient enumerations is that in regular enumerations, if the previous enumeration member is considered a constant, then the member without an initializer will be considered a constant. In contrast, ambient (and non-const) enumeration members without initializers are always considered computed.

Object enumeration

const enum EDirection {
  Up,
  Down,
  Left,
  Right,
}
 
const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;

About type reduction

Use typeof

The typeof operator can be used, giving us basic information about the type of value we have at runtime

  • “string”
  • “number”
  • “bigint”
  • “boolean”
  • “symbol”
  • “undefined”
  • “object”
  • “function”

Note: Due to some quirks in Javascript, such as typeof does not return the string null, the result of typeof null is object

& amp; & amp;, ||, if,!

if determines whether it returns true or false through the following, you need to pay attention to undefined and null here.

  • 0
  • NaN
  • “” (empty string)
  • 0n (bigint version zero)
  • null
  • undefined

switch、===、!==、==、!=

TypeScript also uses switch statements and equality checks such as ===, !==, ==, and != to narrow types. For example:

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // We can now call any 'string' method on 'x' or 'y'.
    x.toUpperCase();
          
(method) String.toUpperCase(): string
    y.toLowerCase();
          
(method) String.toLowerCase(): string
  } else {
    console.log(x);
               
(parameter) x: string | number
    console.log(y);
               
(parameter) y: string | boolean
  }
}

Use in operator

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}

instanceof zoom out

JavaScript has an operator for checking whether a value is an “instance” of another value. More specifically, in JavaScript, x instanceof Foo checks whether x’s prototype chain contains Foo.prototype. While we won’t go into that in depth here, and you’ll see more when we get into classes, they are still useful for most values that can be constructed with new. As you might have guessed, instanceof is also a type guard, and TypeScript shrinks the branches protected by instanceof.

function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString());
               
(parameter) x: Date
  } else {
    console.log(x.toUpperCase());
               
(parameter) x: string
  }
}

Use type predicate is

To define user-defined type protection, we just need to define a function whose return type is a type predicate

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}


//transfer
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

Use assertions

Type narrowing using assertion functions as

Discriminant Union

interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

//Our general way of writing is
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.
  }
}

The above is my general writing method. At this time, we will encounter that rect is a non-required field. At this time, we will encounter an error. Generally, I use the following method to make changes.

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius! ** 2;
  }
}

This time I found that the above method uses a non-null assertion (!) which is actually not friendly. It avoids reducing the use of assertions and reducing code errors. We can re-plan our interface

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;

//use
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
                      
(parameter) shape: Circle
  }
}

//Note that if there are many situations where the code needs to be clearly judged, switch can be used directly

never type

You can use the never type to represent states that should not exist. The never type can be assigned to every type. However, no type can be assigned to never (except never itself). This means that you can use narrowing and rely on the presence of never to perform exhaustive checks in switch statements

For example, adding default to our getArea function, trying to assign the shape to never will not throw an error when all possible cases have been handled.

type Shape = Circle | Square;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}