zoukankan      html  css  js  c++  java
  • 【TS】558- 5000 多字,让你一文掌握 TS 枚举

    创建了一个“重学TypeScript”的微信群,想加群的小伙伴,加我微信 "semlinker",备注重学TS。

    一、基础知识

    在 JavaScript 中布尔类型的变量含有有限范围的值,即 truefalse。而在 TypeScript 中使用枚举,你也可以自定义相似的类型。

    1.1 数字枚举

    这是一个枚举的简单示例:

    enum NoYes {
      No,
      Yes,
    }
    

    NoYes 被称为枚举 NoYes 的成员。与对象字面量一样,尾随逗号是被允许的。对于 NoYes 枚举我们能够轻易的访问它的成员,比如:

    function toChinese(value: NoYes) {
      switch (value) {
        case NoYes.No:
          return '否';
        case NoYes.Yes:
          return '是';
      }
    }
    
    assert.equal(toChinese(NoYes.No), '否');
    assert.equal(toChinese(NoYes.Yes), '是');
    

    1.1.1 枚举成员值

    每个枚举成员都有一个 name 和一个 value。数字枚举成员值的默认类型是 number 类型。也就是说,每个成员的值都是一个数字:

    enum NoYes {
      No,
      Yes,
    }
    
    assert.equal(NoYes.No, 0);
    assert.equal(NoYes.Yes, 1);
    

    除了让 TypeScript 为我们指定枚举成员的值之外,我们还可以手动赋值:

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

    这种通过等号的显式赋值称为 initializer。如果枚举中某个成员的值使用显式方式赋值,但后续成员未显示赋值, TypeScript 会基于当前成员的值加 1 作为后续成员的值,比如以下 Enum 枚举中的成员 C:

    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]
    );
    

    1.2 枚举成员名称的转换

    常量的命名有几种约定:

    • 传统上,JavaScript 使用全大写的名称,这是它从 Java 和 C 继承的约定:Number.MAX_VALUE

    • 众所周知的 Symbol 用驼峰式表示,并以小写字母开头,因为它们与属性名称相关:Symbol.asyncIterator

    • TypeScript 手册使用以大写字母开头的驼峰式名称。这是标准的 TypeScript 风格,我们将其用于 NoYes 枚举。

    1.3 引用枚举成员名称

    与 JavaScript 对象类似,我们可以使用方括号来引用包含非法字符的枚举成员:

    enum HttpRequestField {
      'Accept',
      'Accept-Charset',
      'Accept-Datetime',
      'Accept-Encoding',
      'Accept-Language',
    }
    
    assert.equal(HttpRequestField['Accept-Charset'], 1);
    

    1.4 基于字符串的枚举

    除了数字枚举,我们还可以使用字符串作为枚举成员值:

    enum NoYes {
      No = 'No',
      Yes = 'Yes',
    }
    
    assert.equal(NoYes.No, 'No');
    assert.equal(NoYes.Yes, 'Yes');
    

    对于纯字符串枚举,我们不能省略任何初始化程序。

    1.5 异构枚举

    最后一种枚举称为异构枚举。异构枚举的成员值是数字和字符串的混合:

    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]
    );
    

    请注意,前面提到的规则也适用于此:如果先前的成员值为数字,则我们能省略初始化程序。异构枚举由于其应用较少而很少使用。

    目前 TypeScript 只支持将数字和字符串作为枚举成员值。不允许使用其他值,比如 symbols。

    二、指定枚举成员值

    TypeScript 区分了三种指定枚举成员值的方式:

    • 使用字面量进行初始化:

      • 隐式指定;

      • 通过数字字面量或字符串字面量。

    • 常量枚举成员通过可在编译时计算其结果的表达式初始化。

    • 计算的枚举成员可通过任意表达式初始化。

    2.1 字面量枚举成员

    如果枚举只有字面量成员,我们可以将这些成员用作类型(类似于数字字面量可以用作类型的方式):

    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);
    

    此外,字面量枚举支持完整性检查(我们将在后面进行介绍)。

    TypeScript 2.6 支持在 .ts 文件中通过在报错一行上方使用 // @ts-ignore 来忽略错误。

    // @ts-ignore 注释会忽略下一行中产生的所有错误。建议实践中在 @ts-ignore之后添加相关提示,解释忽略了什么错误。

    请注意,这个注释仅会隐藏报错,并且我们建议你少使用这一注释。

    2.2 const 枚举成员

    如果可以在编译时计算枚举成员的值,则该枚举成员是常量。因此,我们可以隐式指定其值(即,让 TypeScript 为我们指定它的值)。或者我们可以显式指定它的值,并且仅允许使用以下语法:

    • 数字字面量或字符串字面量

    • 对先前定义的常量枚举成员的引用

    • 括号

    • 一元运算符 +-~

    • 二进制运算符 +-*/%<<>>>>>&|^

    以下是一个成员都是常量的枚举示例:

    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,
    }
    

    如果枚举仅含有常量成员,则不能再将成员用作类型。但是我们仍然可以进行完整性检查。

    2.3 计算枚举成员

    可以通过任意表达式设置枚举成员的值。例如:

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

    这是一个数字枚举。字符串枚举和异构枚举会有更多的限制。例如,我们不能调用某些方法来设定枚举成员的值:

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

    三、数字枚举的缺点

    3.1 缺点:日志输出

    在输出数字枚举的成员时,我们只会看到数字:

    enum NoYes { No, Yes }
    
    console.log(NoYes.No);
    console.log(NoYes.Yes);
    
    // Output:
    // 0
    // 1
    

    3.2 缺点:松散型检查

    将枚举用作类型时,允许的值不只是枚举成员的值 – 可以接受任何数字:

    enum NoYes { No, Yes }
    function func(noYes: NoYes) {}
    
    func(33); // no error!
    

    为什么没有更严格的静态检查?Daniel Rosenwasser解释:

    该行为是由按位运算引起的。有时 SomeFlag.Foo | SomeFlag.Bar 打算产生另一种 SomeFlag。相反,您最终得到了 number,并且你不想回退到 SomeFlag

    我认为,如果我们再次运行 TypeScript 之后仍然有枚举,那么我们将为位标志建立一个单独的构造。


    继续阅读:遇到这些 TS 问题你会头晕么?

    3.3 建议:使用字符串枚举

    我的建议是使用字符串枚举:

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

    一方面,日志输出对人类更友好:

    console.log(NoYes.No);
    console.log(NoYes.Yes);
    
    // Output:
    // 'No'
    // 'Yes'
    

    另一方面,我们得到更严格的类型检查:

    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');
    

    四、枚举的用例

    4.1 用例:位模式

    在 Node.js 文件系统模块中,几个函数具有参数模式。它的值用于通过 Unix 保留的编码来指定文件权限:

    • 为三类用户指定了权限:

      • 用户:文件的所有者

      • 组:与文件关联的组的成员

      • 全部:所有人

    • 对于每个类别,可以授予以下权限:

      • r(读取):允许类别中的用户读取文件

      • w(写):允许类别中的用户更改文件

      • x(执行):允许类别中的用户执行文件

    这意味着权限可以用 9 位表示(3 个类别,每个类别具有 3 个权限):


    用户所有
    权限r,w,xr,w,xr,w,x
    8、7、65 4 32 1 0

    虽然在 Node.js 不是这样做,但是我们可以使用一个枚举来处理这些标志:

    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,
    }
    

    位模式通过按位或(OR)组合:

    // 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);
    

    八进制,Octal,缩写 OCT 或 O,一种以 8 为基数的计数法,采用 0,1,2,3,4,5,6,7 八个数字,逢八进 1。八进制 0o755 对应的十进制值是 493。

    继续阅读:一文读懂原码、反码与补码

    4.1.1 对位模式的替代

    位模式背后的主要思想是存在一组标志,并且可以选择这些标志的任何子集。因此,使用 Set 选择子集是执行同一任务的一种更具描述性的方式:

    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!');
    

    4.2 用例:多个常量

    有时,我们有一组属于同类型的常量:

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

    这是一个很好的枚举用例:

    enum LogLevel {
      off = 'off',
      info = 'info',
      warn = 'warn',
      error = 'error',
    }
    

    该枚举的好处是:

    • 常量名称被分组并嵌套在命名空间 LogLevel 内。

    • LogLevel 只要需要这些常量之一,就可以使用类型,并且 TypeScript 会执行静态检查。

    4.3 用例:相比布尔值来说更具自我描述性

    当使用布尔值表示替代方案时,枚举通常是一种更具自我描述性的选择。

    4.3.1 布尔型示例:有序列表与无序列表

    例如,为了表示列表是否有序,我们可以使用布尔值:

    class List1 {
      isOrdered: boolean;
      // ···
    }
    

    但是,枚举更具有自我描述性,并具有其他好处,即如果需要,我们可以在以后添加更多选择项。

    enum ListKind { ordered, unordered }
    
    class List2 {
      listKind: ListKind;
      // ···
    }
    
    4.3.2 布尔型示例:失败与成功

    同样,我们可以通过布尔值或枚举来表示操作是成功还是失败:

    class Result1 {
      success: boolean;
      // ···
    }
    
    enum ResultStatus { failure, success }
    
    class Result2 {
      status: ResultStatus;
      // ···
    }
    

    4.4 用例:更安全的字符串常量

    考虑以下创建正则表达式的函数。

    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);
    

    若使用基于字符串的枚举更为方便:

    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);
    

    五、运行时枚举

    TypeScript 将枚举编译为 JavaScript 对象。例如,定义以下枚举:

    enum NoYes {
      No,
      Yes,
    }
    

    TypeScript 将该枚举编译为:

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

    在此代码中,进行了以下赋值操作:

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

    有两组赋值操作:

    • 前两个赋值语句将枚举成员名称映射到值。

    • 后两个赋值语句将值映射到名称。这称为反向映射,我们将在后面介绍。

    5.1 反向映射

    给定一个数字枚举:

    enum NoYes {
      No,
      Yes,
    }
    

    普通的映射是从成员名称到成员值:

    // 静态查找
    assert.equal(NoYes.Yes, 1);
    
    // 动态查找
    assert.equal(NoYes['Yes'], 1);
    

    数字枚举还支持从成员值到成员名称的反向映射:

    assert.equal(NoYes[1], 'Yes');
    

    5.2 运行时基于字符串的枚举

    基于字符串的枚举在运行时具有更简单的表示形式。

    考虑以下枚举:

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

    它会被编译为以下 JavaScript 代码:

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

    TypeScript 不支持基于字符串枚举的反向映射。

    六、const 枚举

    如果枚举以 const 关键字为前缀,则在运行时没有任何表示形式,而是直接使用成员的值。

    6.1 编译非 const 枚举

    首先我们来看一下非 const 枚举:

    enum NoYes {
      No,
      Yes,
    }
    
    function toChinese(value: NoYes) {
      switch (value) {
        case NoYes.No:
          return '否';
        case NoYes.Yes:
          return '是';
      }
    }
    

    TypeScript 会将以上代码编译为:

    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 '否';
            case NoYes.Yes:
                return '是';
        }
    }
    

    6.2 编译 const 枚举

    这与前面的代码基本一致,但是使用了 const 关键字:

    const enum NoYes {
      No,
      Yes,
    }
    
    function toChinese(value: NoYes) {
      switch (value) {
        case NoYes.No:
          return '否';
        case NoYes.Yes:
          return '是';
      }
    }
    

    现在,之前生成的 NoYes 对象消失了,仅保留了其成员的值:

    function toChinese(value) {
        switch (value) {
            case 0 /* No */:
                return '否';
            case 1 /* Yes */:
                return '是';
        }
    }
    

    七、编译时枚举

    7.1 枚举是对象

    TypeScript 将(非 const)枚举视为对象:

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

    7.2 字面量枚举全面性检查

    当我们接受一个枚举成员值时,我们通常要确保:

    • 我们没有收到非法的值;

    • 我们没有遗漏任何枚举成员的值。(如果以后再添加新的枚举成员时,这一点尤为重要。)

    7.2.1 抵御非法值

    在以下代码中,我们针对非法值采取了两种措施:

    enum NoYes {
      No = 'No',
      Yes = 'Yes',
    }
    
    function toChinese(value: NoYes) {
      switch (value) {
        case NoYes.No:
          return '否';
        case NoYes.Yes:
          return '是';
        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"$/);
    

    这些措施是:

    • 在编译时,该类型 NoYes 可防止将非法值传递给 value 参数;

    • 在运行时,如果含有其它值,则 default 分支会抛出异常。

    7.2.2 通过全面性检查抵御遗漏场景

    我们可以再采取一种措施。以下代码执行全面性检查:如果我们忘记考虑所有枚举成员,TypeScript 将警告我们。

    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 '否';
        case NoYes.Yes:
          return '是';
        default:
          throwUnsupportedValue(value);
      }
    }
    

    全面性检查如何工作?对于每种情况,TypeScript 都会推断 value 的类型:

    function toGerman2b(value: NoYes) {
      switch (value) {
        case NoYes.No:
          const x: NoYes.No = value;
          return '否';
        case NoYes.Yes:
          const y: NoYes.Yes = value;
          return '是';
        default:
          const z: never = value;
          throwUnsupportedValue(value);
      }
    }
    

    在 default 分支中,TypeScript 会推断 value 的类型为 never 类型。但是,如果我们添加一个成员 MaybeNoYes 枚举中,之后 value 的推断类型是 NoYes.Maybe,这时该变量的类型与 throwUnsupportedValue() 方法中参数的类型在静态上不兼容。因此,我们在编译时会收到以下错误消息:

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

    幸运的是,这种全面性检查也适用于以下 if 语句:

    function toGerman3(value: NoYes) {
      if (value === NoYes.No) {
        return '否';
      } else if (value === NoYes.Yes) {
        return '是';
      } else {
        throwUnsupportedValue(value);
      }
    }
    
    7.2.3 全面性检查的另一种方法

    另外,如果我们为以下 toChinese() 函数指定返回类型,也可以实现全面性检查:

    enum NoYes {
      No = 'No',
      Yes = 'Yes',
    }
    
    function toChinese(value: NoYes): string {
      switch (value) {
        case NoYes.No:
          const x: NoYes.No = value;
          return '否';
        case NoYes.Yes:
          const y: NoYes.Yes = value;
          return '是';
      }
    }
    

    如果我们向 NoYes 中添加成员,则 TypeScript 会提醒 toChinese() 方法可能会返回 undefined

    这种方法的缺点:这种方法不适用于 if 语句。

    继续阅读:TS 如何进行完整性检查

    7.3 keyof 和枚举

    我们可以使用 keyof 类型运算符创建类型,其元素是枚举成员的 key。当我们这样做,我们需要结合 keyoftypeof 一起使用:

    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) {
      // ···
    }
    

    为什么这样?这比直接定义 HttpRequestKey 类型更方便。

    继续阅读:TypeScript keyof 操作符

    7.3.1 使用 keyof 不使用 typeof

    如果使用 keyof 不使用 typeof,则会得到另一个不太有用的类型:

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

    keyof HttpRequestKeyEnum 的结果与 keyof number 相同。

    继续阅读:TypeScript typeof 操作符

    本文主要参考了“德国阮一峰” —— Axel Rauschmayer 大神的 numeric-enums 这篇文章,感兴趣的小伙伴可阅读原文哟。

    https://2ality.com/2020/01/typescript-enums.html#numeric-enums

    八、参考资源

    • typescript-enums

    原创系列推荐

    1. JavaScript 重温系列(22篇全)

    2. ECMAScript 重温系列(10篇全)

    3. JavaScript设计模式 重温系列(9篇全)

    4. 正则 / 框架 / 算法等 重温系列(16篇全)

    5. Webpack4 入门(上)|| Webpack4 入门(下)

    6. MobX 入门(上) ||  MobX 入门(下)

    7. 59篇原创系列汇总

    回复“加群”与大佬们一起交流学习~

    点这,与大家一起分享本文吧~

    个人博客:http://www.pingan8787.com 微信公众号【前端自习课】和千万网友一起,每日清晨,享受一篇前端优秀文章。 目前已连续推送文章 600+ 天,愿每个人的初心都能一直坚持下去!
  • 相关阅读:
    spring学习总结003 --- IOC容器启动源码(BeanFactoryPostProcessor)
    spring学习总结002 --- IOC容器启动源码(BeanFactory)
    ubuntu上安装mysql
    kafka学习总结017 --- consumer配置参数之max.poll.interval.ms
    kafka学习总结016 --- consumer配置参数session.timeout.ms和heartbeat.interval.ms
    kafka学习总结015 --- consumer配置参数之auto.offset.reset
    kafka学习总结014 --- consumer多线程问题
    kafka学习总结013 --- kafka消费者API
    kafka学习总结012 --- 数据消费相关流程
    Java SAX解析
  • 原文地址:https://www.cnblogs.com/pingan8787/p/13069420.html
Copyright © 2011-2022 走看看