Dark Dwarf Blog background

TypeScript 高级类型:字面量与枚举

TypeScript 高级类型:字面量与枚举

1. 字面量类型

字面量类型 (Literal Types) 允许我们将一个变量的类型指定为某个具体的“字面量”值,而不仅仅是 stringnumber。这是一种非常精确的类型约束。

a.a. 字符串字面量类型

这是最常见的字面量类型,它将一个变量的可能值限制为几个特定的字符串之一。通常与类型别名(type)和联合类型(|)结合使用。

// 定义一个字符串字面量类型
type LogLevel = "debug" | "info" | "warn" | "error";

function logger(message: string, level: LogLevel) {
  console.log(`[${level.toUpperCase()}]: ${message}`);
}

logger("User logged in", "info"); // OK
// Error: Argument of type '"critical"' is not assignable to parameter of type 'LogLevel'.
logger("System crash", "critical");

字符串字面量类型是纯粹的编译时构造。它们在编译后的 JavaScript 代码中会被完全擦除,不产生任何额外的运行时开销。这使得它们在 Web 开发中对控制包体积非常友好。

b.b. 模板字面量类型

模板字面量类型是字符串字面量类型的扩展,它允许我们通过模板字符串的语法来构造和匹配类型,极大地增强了类型的表达能力。

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;

// 结合起来,创建一个更具体的类型
type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;

let endpoint: ApiEndpoint = "GET /api/users"; // OK
let endpoint2: ApiEndpoint = "POST /api/products"; // OK

// Error: Type '"FETCH /api/data"' is not assignable to type 'ApiEndpoint'.
let invalidEndpoint: ApiEndpoint = "FETCH /api/data";

c.c. 数字与布尔值字面量类型

字面量类型同样适用于数字和布尔值。

type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type ResponseStatus = 200 | 404 | 500;
type AdminFlag = true;

function checkStatus(status: ResponseStatus) {
  /* ... */
}
checkStatus(200); // OK
// checkStatus(301); // Error

2. 枚举

枚举(enum)是 TypeScript 从 C# 等语言借鉴而来的特性,用于定义一个命名的常数集合。它在 TypeScript 中是一个真实存在的运行时对象

a.a. 枚举的底层原理

理解枚举的关键在于看它被编译成 JavaScript 后的样子。

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

编译后的 JavaScript (ES5):

var Direction;
(function (Direction) {
  Direction[(Direction["Up"] = 0)] = "Up";
  Direction[(Direction["Down"] = 1)] = "Down";
  Direction[(Direction["Left"] = 2)] = "Left";
  Direction[(Direction["Right"] = 3)] = "Right";
})(Direction || (Direction = {}));

这段代码创建了一个名为 Direction 的对象,并巧妙地通过一个立即执行函数表达式 (IIFE) 来初始化它。Direction[Direction["Up"] = 0] = "Up"; 这行代码做了两件事:

  1. Direction["Up"] = 0: 创建了从名称到值的映射 (Direction.Up 值为 0)。
  2. Direction[0] = "Up": 创建了从值到名称的反向映射 (Direction[0] 值为 "Up")。

这就是为什么数字枚举既可以正向访问也可以反向访问的原因。

b.b. 枚举的分类

i.i. 数字枚举

默认情况下,枚举是基于数字的。第一个成员默认为 0,后续成员依次递增。

enum Status {
  Pending, // 0
  InProgress, // 1
  Done, // 2
}

console.log(Status.InProgress); // 1
console.log(Status[1]); // "InProgress" (反向映射)

我们也可以手动指定成员的值,未指定的成员会从上一个成员的值开始递增。

enum HttpCode {
  OK = 200,
  NotFound = 404,
  InternalError, // 会被自动赋值为 405
}

ii.ii. 字符串枚举 (String Enums)

字符串枚举的每个成员都必须用字符串字面量或另一个字符串枚举成员进行初始化。

enum LogLevel {
  Debug = "DEBUG",
  Info = "INFO",
  Warn = "WARN",
}

console.log(LogLevel.Info); // 'INFO'

字符串枚举没有反向映射。这通常是期望的行为,因为它让枚举对象在运行时更纯粹、更可预测。

iii.iii. const enum: 性能优化

为了消除枚举在运行时产生的额外代码和性能开销,TypeScript 提供了 const enum

const enum ConstDirection {
  Up,
  Down,
}

let dir = ConstDirection.Up;

编译后的 JavaScript:

var dir = 0; /* Up */

const enum 的成员会在编译时被直接内联到使用它们的地方。ConstDirection 对象本身在编译后完全消失。这提供了与字符串字面量类型相似的零运行时成本,但它不能包含计算成员(也就是运行才能知道结果的表达式,如 .length 属性)。