# 原始类型-枚举类型

本节介绍 TypeScript 对 JavaScript 扩充的为数不多的语言特性枚举。

# 枚举概述

枚举是一个在强类型语言中很常见的特性例如 C。它类似字面量, 也是基于有限的原始值创建,只是语义更友好!

采用 enum 枚举名 { 标识符1, 标识符2,...} 定义枚举,枚举内的标识符表示 枚举成员(enum member)。







































 














// 申明一个枚举变量 Direction
enum Direction {
  Right, // 标识符,合法的变量名即可 指代右
  Left, // 标识符 指代左
  Forward, // 标识符 指代前
  Backward // 标识符 指代后
}

// 申明移动函数只接受 Direction 的枚举类型,且无返回值
function move(direction: Direction): void {
  switch (direction) {
    // 采用 枚举名.标识符 的方式引用枚举的值
    case Direction.Left:
      console.log('left');
      break;
    case Direction.Right:
      console.log('right');
      break;
    case Direction.Backward:
      console.log('backward');
      break;
    case Direction.Forward:
      console.log('forward');
      break;
    default:
      console.log('stay');
  }
}

// 定义一个枚举变量,赋值为 Direction.Left
let moveLeft: Direction = Direction.Left;
move(moveLeft);

// 也可直接使用 枚举名.标识符 的方式赋值
move(Direction.Right);
move(Direction.Forward);
move(Direction.Backward);

console.log(Direction);
/** 枚举类型的实际结果是一个基于标识符和默认初始值的映射和反映射
{ '0': 'Right',
  '1': 'Left',
  '2': 'Forward',
  '3': 'Backward',
  Right: 0,
  Left: 1,
  Forward: 2,
  Backward: 3 } 
*/

// 基于上述输出你也可以反向获取枚举成员的名称
console.log(Direction[0]); // 返回 Right

注意示例中 Direction 的打印结果,枚举申明 enum 不仅创建了类型,同时会编译出实际的值,而字面量类型只会创建类型无法产生值,这是它们的核心区别。

TIP

当需要引用实际的值时使用枚举,若只是单纯的需要限制变量的取值采用字面量类型。

除了枚举成员到值,编译结果也包含值到枚举成员的映射,这是 反向映射(Reverse mappings), 可以利用此特性利用值获取枚举成员名。

# 枚举的只读特性

枚举类型默认为只读,当试图对枚举值进行修改时,Typescript 静态检查会提示静态类型无法修改。

enum READONLY_ENUM {
  A = 'a',
  B = 'b'
}

class C1_USE_ENUM {
  static get READONLY_ENUM() {
    return  READONLY_ENUM
  }
}

var types = C1_USE_ENUM.READONLY_ENUM;

// ts throw error can't change enum value
types.A = 'g';

也可以使用 常量断言

# 枚举的类型

TypeScript 会默认给枚举变量的标识符绑定从 0 开始的自增数值。 你也可以人为修改枚举成员的取值。根据所取得值可细分为

  • 数值枚举(Numeric enums) 枚举成员的值为数字(默认行为)
  • 字符串枚举(String enums) 枚举成员的值为字符串
  • 异构枚举(Heterogeneous enums) 枚举成员的值为数值或字符串
// 采用规则 1 a = 0 ,后续值自增 b =1,c =2
enum DefaultEnum {
  a,
  b,
  c
}

// 采用规则 2
enum FooEnum {
  Foo = 'foo',
  Bar = 10,
  Bar1, // Bar1 之前的值为 10 所以 Bar1 为 11
  Bar2, // Bar2 自增为 12
  FooBar = 'bar',
  FooBar1 = 10,
  FooBar2 // FooBar2 之前的值为 10 所以 FooBar2 为 11
}

console.log(DefaultEnum, FooEnum);
/**
 * DefaultEnum:
 * { '0': 'a', '1': 'b', '2': 'c', a: 0, b: 1, c: 2 } 
 * 
 * FooEnum:
 { 
  '10': 'FooBar1',
  '11': 'FooBar2',
  '12': 'Bar2',
  Foo: 'foo',
  Bar: 10,
  Bar1: 11,
  Bar2: 12,
  FooBar: 'bar',
  FooBar1: 10,
  FooBar2: 11 }
 */

注意输出的结果包含了

# 默认值的规则

之前示例中 TypeScript 对于未赋值的枚举成员会自动计算值,你只需要记住如下规则:

  1. 当所有枚举成员都没有赋值时,默认从 0 开始,自增赋值
  2. 当枚举成员之前的值为数值 n 时(对于起始位置则 n = 0),后续默认值从 n 开始自增赋值














 












// 采用表达式对枚举成员进行赋值
enum FileDemo {
  None, // 默认值为 0
  Read = 1 << 1, // 1 左移一位, Read = 2
  Write = 1 << 2, // 1 左移两位, Write = 4
  ReadWrite = Read | Write, // 引用之前的枚举成员的值进行或逻辑 ReadWrite = 6

  // 使用一个复杂表达式进行赋值,将数组的累加结果赋值给 G
  G = [1, 2, 3, 4].reduce((sum, ele) => (sum += ele)) // G =  10
}

console.log(FileAccess);
/**
 * FileAccess: 
 { 
 '0': 'None',
  '2': 'Read',
  '4': 'Write',
  '6': 'ReadWrite',
  '10': 'G',
  None: 0,
  Read: 2,
  Write: 4,
  ReadWrite: 6,
  G: 10 }
 */

注意高亮的行,由于 FooBar1 = 10 会导致后申明的成员变量替换 Bar = 10 的反向映射结果,即 10: FooBar1 取代 10: Bar。所以在申明是避免数值的重复。

那上述规则有什么实际意义呢?在业务开发中,为了更加友好的提示用户, 我们会定义一系列的业务状态码,若采用连续状态码会导致后续添加新的状态时不好分类,所以一般会将状态划分为区间方便后续补充,例如 0-99 为用户错误,100-199 为服务端错误等(实际上 HTTP 的状态码设计就是这样的)。此时就可以利用上述规则简化枚举定义。

// 利用枚举类型默认值的计算规则,将业务状态分段
enum BusinessCode {
  // 用户错误 0-99
  USER_ERR = 0, // 通用用户错误
  PASSWORD_ERR, // 密码错误
  // ...

  // 服务端错误 100-199
  SERVER_ERR = 100, // 通用服务器错误
  DB_ERR, // 数据库错误
  // ...

  // 网络异常 200-299
  NET_ERR = 200 // 通用网络错误
  // ...
}

# 计算赋值

除了直接赋值常量,枚举成员也可采用表达式计算赋值,这进一步增加了灵活性。

// 采用表达式对枚举成员进行赋值
enum FileDemo {
  None, // 默认值为 0
  Read = 1 << 1, // 1 左移一位, Read = 2
  Write = 1 << 2, // 1 左移两位, Write = 4
  ReadWrite = Read | Write, // 引用之前的枚举成员的值进行或逻辑 ReadWrite = 6

  // 使用一个复杂表达式进行赋值,将数组的累加结果赋值给 G
  G = [1, 2, 3, 4].reduce((sum, ele) => (sum += ele)) // G =  10
}

console.log(FileAccess);
/**
 * FileAccess: 
 { 
 '0': 'None',
  '2': 'Read',
  '4': 'Write',
  '6': 'ReadWrite',
  '10': 'G',
  None: 0,
  Read: 2,
  Write: 4,
  ReadWrite: 6,
  G: 10 }
 */

表达式枚举的一个典型用例就是基于位操作的逻辑判断。在此之前你可以阅读 MDN 位操作 (opens new window) 熟悉相关语法。

我们知道文件系统存在可读,可写,可执行的权限。在类 Unix 系统多用 如下符号表述 w(可读),r(可写),x(可执行),-(没有该项权限) 对于一个文件这些权限是可以叠加的,如何表示上述状态,采用表格显示如下

权限 有 无
可执行 x -
可写 w -
可读 r -

我们看到每个权限只分为有和无,这种二元性正好可以利用二进制的位进行表达。

二级制 0 1
第一位(表示 x) ▫️▫️▫️ 无权限 ▫️▫️▪️ 有
第二位(表示 w) ▫️▫️▫️ 无权限 ▫️▪️▫️ 有
第三位(表示 r) ▫️▫️▫️ 无权限 ▪️▫️▫️ 有

上述逻辑正好可以利用枚举和表达式赋值表示。

enum FileAccess {
  Execution = 1,
  Write = 1 << 1, // 左移一位表示读权限
  Read = 1 << 2 // 左移一位表示写权限
}

// 显示文件权限
function getFileAccess(num: number): string {
  let res = ['-', '-', '-']; // 默认无权限
  // 与逻辑判断对应标志位是否有读权限
  if (num & FileAccess.Read) {
    res[0] = 'r';
  }
  // 与逻辑判断对应标志位是否有写权限
  if (num & FileAccess.Write) {
    res[1] = 'w';
  }
  // 与逻辑判断对应标志位是否有执行权限
  if (num & FileAccess.Execution) {
    res[2] = 'x';
  }
  return res.join('');
}
// 创建可读可写的文件
let readWritFile = FileAccess.Write | FileAccess.Read;
// 包含所有权限的文件
let allAccessFile = FileAccess.Execution | FileAccess.Write | FileAccess.Read;

// 返回: readWritFile: rw-
console.log('readWritFile:', getFileAccess(readWritFile));

// 返回: allAccessFile: rwx
console.log('allAccessFile:', getFileAccess(allAccessFile));

注意计算赋值只针对数值枚举类型

TIP

在涉及多个二元可叠加状态的操作时,尝试使用位运算和枚举的计算属性 精简代码逻辑

# 常量枚举

为了避免 TypeScript 对枚举类型创建实际的变量。 可以采用 const enum 申明常量枚举

// 数值常量枚举
const enum ConstDirection {
  Right,
  Left,
  Forward,
  Backward
}
// 只能使用成员的值
console.log(ConstDirection.Right); // 该处编译结果被替换为 0
console.log(ConstDirection); // 报错,但编译结果不会变化

const enum ConstDirectionStr {
  Up = 'UP',
  Down = 'DOWN'
}
// 只能使用成员的值
console.log(ConstDirectionStr.Up); // 该处编译结果被替换为 'UP'
console.log(ConstDirectionStr); // 报错,但编译结果不会变化

// 异构枚举, 每个标识符可以为 string 或 number
const enum ConstHeterogeneousEnums {
  Yes = 'yes',
  No = 0,
  Error, // 注意由于之前的值为 0,此处 TypeScript 可以推导进行自增所以值为 1
  Success // 推导自增值为 2
}
// 只能使用成员的值
console.log(ConstHeterogeneousEnums.Yes); // 该处编译结果被替换为 'yes'
console.log(ConstHeterogeneousEnums); // 报错,但编译结果不会变化

常量枚举类型会直接替换代码中引用常量成员的地方为对应的值。 注意由于常量枚举并未创建枚举对象,所以不支持直接访问枚举对象和反向映射。

TIP

通过编译时传入 --preserveConstEnums 参数可以使常量枚举也创建枚举对象

# 总结

利用枚举特性,我们可以更加语义化的定义一系列原始值。 知识点如下:

  1. 采用 enum 枚举名 {标识符1, 标识符2, ...} 定义枚举类型,采用 枚举名.标识符 引用枚举成员,枚举类型会被编译为对象
  2. 根据枚举成员的值,分为三种枚举类型
    1. 数值枚举 值只能为 number 类型
    2. 字符串枚举 值只能为 string 类型
    3. 异构枚举 值可以为 number 或 string 类型
  3. 枚举成员的默认值遵循如下规则
  4. 当所有枚举成员都没有赋值时,默认从 0 开始,自增赋值
  5. 当枚举成员之前的值为数值 n 时(对于起始位置则 n = 0),后续默认值从 n 开始自增赋值

利用上述规则精简分段的状态码逻辑

  1. 数值枚举的值可以为表达式,利用此特性优化位逻辑的状态处理
  2. 采用 const enum {} 申明常量枚举类型
    1. 常量类型会在编译后替换位置
    2. 常量类型只能访问枚举成员,不支持访问枚举对象和反向映射
    3. 采用 --preserveConstEnums 是常量类型编译结果包含枚举对象