TypeScript字符串枚举,以及何时和如何使用它们

1,152 阅读16分钟

基于字符串的枚举介绍

在本节中,我们将探讨TypeScript中的字符串枚举。

如果你是JavaScript/TypeScript的新手,你可能会想知道Enums是什么。enums 关键字为我们提供了一种定义有限值集的方式--通常是以强类型的方式命名常量。它们还允许我们为一个特定的集合集或类型指定一个值列表。

枚举在JavaScript中不被原生支持,但有一个变通方法,让我们在语言中使用Object.freeze 构造来复制它们的行为,因为TypeScript把枚举当作真实的对象(尽管在运行时适用于非常量枚举)。我们可以像下面的例子那样使用这个结构。

const directionEnum = Object.freeze({ 
    UP :  "UP", 
    DOWN:  "DOWN" 
});  

console.log(directionEnum) //{ UP: 'UP', DOWN: 'DOWN' } 

这样做,我们得到一个只读和受保护的对象。注意只有当我们把鼠标悬停在directionEnum const上时,才会显示只读定义。

const directionEnum: Readonly<{ 
    UP: string; 
    DOWN: string; 
}> 

Object.freeze 方法可以防止修改现有的属性和值,也可以防止增加新的属性。这有点反映了枚举背后的想法,因为它们的目的是为一个给定的变量声明提供一定数量的固定值。

枚举并不是编程中的一个新概念。我们可能已经知道,大多数编程语言如Java、C等都在其核心中定义了枚举的概念。我们也可以把枚举想象成人为创建的包含有限数值的类型,就像我们有布尔类型,它只包含falsetrue

由于枚举在其他语言中的实用性和优势,枚举被引入并添加到TypeScript中。然而,枚举对于TS来说是非常独特和特殊的,因为它们不是JavaScript语言的类型化扩展,也不等同于JavaScript语言的特定功能,尽管TS是JS的类型化超集。

注意:目前ECMAScript有一个阶段性的建议,在JavaScript中加入枚举。更多的细节在这个 repo 中。

基于数字的枚举介绍

枚举默认是基于数字的。对于基于数字的枚举,由于其自动递增的特性,成员之间是不同的。它们可以有一个初始化器(即我们明确指定枚举成员的值),也可以没有。

让我们看看下面的一个例子。

enum statusEnumWithInitializer = { 
"OPEN" = 10, 
"CLOSE", 
} 
//statusEnumWithInitializer.CLOSE = 11  
//Since the following members are auto incremented from that point on.  

如果我们不使用初始化器,我们就有下面这个。

enum statusEnumWithoutInitializer = { 
"OPEN", 
"CLOSE", 
} 
//statusEnumWithoutInitializer.OPEN = 0 
//statusEnumWithoutInitializer.CLOSE = 1 

从上面的例子我们可以看出,访问枚举的成员就像访问对象的属性一样简单。我们在前面提到,这样做的原因在于非const枚举具有类似于对象的行为。

注意上面的代码片段中的尾部逗号。在现实世界中使用这些枚举类型是非常直接的,因为我们可以简单地将它们声明为类型并作为参数传递给函数。

由于基于数字的枚举的自动递增行为,这些枚举在没有初始化器时存在一个注意事项。它们要么需要先声明,要么必须继承基于数字的常量初始化的数字枚举,这在异质枚举中也适用。这是因为枚举需要在编译时被完全评估

我们什么时候需要基于字符串的枚举?

通常情况下,当我们打算声明某些必须满足枚举声明中定义的某些标准的类型时,枚举类型就会派上用场。正如我们前面提到的,虽然枚举默认是基于数字的,但TypeScript ≥ 2.4版本支持基于字符串的枚举。

基于字符串的枚举,就像对象字面一样,支持使用方括号符号的计算名称,而基于数字的枚举通常不是这样的。因此,这限制了我们在JSON对象中使用基于数字的枚举的能力,因为通常不可能计算这些类型的枚举成员的名称。

字符串枚举在传输协议中是可序列化的,并且很容易调试--因为它们毕竟只是字符串。它们也允许在运行时有一个有意义和可读的值,与枚举成员的名字无关。

对于完全基于字符串的枚举,我们不能省略任何初始化器,因为所有的枚举成员都必须带有值。但是对于数字枚举来说就不是这样了,它最终只能成为普通的数字,因此可能不是太有用。

在这篇文章中,我们将专注于基于字符串的枚举。有关基于数字的枚举的更多信息,请参考TypeScript文档。如果您需要枚举类型的一般介绍概述枚举在语言中可能被滥用的方式,LogRocket博客将为您介绍。

常量和计算型枚举

基于常量的枚举是有一个没有初始化值的单一成员的枚举。这意味着它们被自动分配的值是0

它们也可以有一个以上的成员值,其中第一个成员必须是一个数字常数。这意味着后面的值是通过在前面的数字常量上加一来增加的,按顺序进行。

总之,对于常量枚举来说,枚举成员值可以是没有初始化器的第一个成员,或者必须有一个初始化器,如果它前面有其他枚举成员的话,那么它的初始化器就是数字。

请注意,常量枚举的值可以在编译时被计算出来,也可以被计算出来。对于计算枚举的情况,它们可以通过表达式进行初始化。请看下面的一个计算枚举的例子。

enum computedEnum { 
 a = 10 
 str = "str".length // computed enum 
 add = 300 + 100 //computer enum 
} 

指定枚举成员的值

我们可以根据需要以下列方式指定枚举成员的值。

  • 字面枚举成员。字面枚举类型是枚举类型,其中每个成员要么没有初始化器,要么有一个初始化器,指定一个数字字面,一个字符串字面,或者一个命名该枚举类型中另一个成员的标识符。值通过数字字面隐式初始化,或通过字符串字面显式初始化。
  • 常量枚举成员。基于常量的枚举成员只能用字符串字面来访问。它们通过表达式进行初始化,表达式可以隐式或显式地使用字符串字面、数字字面、括号等进行指定。
  • 计算枚举成员。我们可以通过任意的表达式来初始化这些成员

字符串字面和联合类型

一般来说,字面类型是JavaScript的原始值。从TypeScript ≥ 1.8版本开始,我们可以创建字符串字面类型。具体来说,字符串字面类型允许我们定义一个类型,只接受一个特定的字符串字面。就其本身而言,它们通常不是那么有用,但当它们与联合类型结合时,它们就变得非常强大。

当它们与联合类型结合使用时,它们模仿了字符串枚举的预期行为,因为它们也为命名的字符串值提供了可靠和安全的体验。请看下面的例子。

type TimeDurations = 'hour' | 'day' | 'week' | 'month'; 

var time: TimeDurations; 
time = "hour"; // valid  
time = "day";  //  valid 
time = "dgdf"; // errors 

从上面看,TimeDurations 类型看起来像一个字符串枚举,因为它定义了几个字符串常量。

枚举类型可以有效地成为每个枚举成员的联合类型。字符串字面和联合类型的组合提供了和枚举一样多的安全性,并且具有更直接地翻译成JavaScript的优势。它还在各种IDE中提供了类似的强大自动完成功能。如果你感到好奇,你可以在TypeScript Playground上快速检查。

对于字符串枚举,我们可以从其值的字面类型中产生一个联合类型,但这并不发生在其他方面。

基于字符串的枚举的使用情况和重要性

基于字符串的枚举在2.4版本中被引入TypeScript,它们使得为枚举成员分配字符串值成为可能。让我们看一下下面文档中的一个例子。

 enum Direction { 
  Up = "UP", 
  Down = "DOWN", 
  Left = "LEFT", 
  Right = "RIGHT", 
} 

在此之前,开发者不得不依靠使用字符串字面和联合类型来描述有限的字符串值集,就像基于字符串的枚举那样。因此,举例来说,我们可以有一个像这样定义的类型。

type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT";  

然后我们可以利用这个类型作为,比如说,一个函数参数,语言可以检查这些精确的类型在编译时是否传递给了函数,当实例化时。

总之,为了利用基于字符串的枚举类型,我们可以通过使用枚举的名称和它们相应的值来引用它们,就像你访问一个对象的属性那样。

在运行时,基于字符串的枚举的行为就像对象一样,可以很容易地像普通对象一样传递给函数。关于枚举在运行时如何表现得像对象的例子,请参见TypeScript文档。

让我们记住,对于基于字符串的枚举,初始化器不能省略,不像基于数字的枚举,提供的初始化器是第一个成员或前面的成员,并且必须有一个数字值。每个枚举成员必须用一个常量来初始化,这个常量要么是字符串字面,要么是另一个枚举成员,它是一个字符串和字符串枚举的一部分。

字符串枚举在JSON对象中被大量使用,用于验证API调用,以确保参数被正确传递。另一个美妙的用例是它们在为预定义的API定义特定领域的值中的应用。

枚举类型的重要性怎么强调都不为过。例如,每当我们在代码中使用一个枚举成员进行验证时,TypeScript会静态检查是否有其他值被使用。

它们对于确保安全的字符串常量也很有用。与使用布尔值相比,枚举提供了一个更加自我描述的选项。相反,我们可以指定该领域所特有的枚举,这使得我们的代码更具描述性。

因此,如果需要的话,我们可以决定以后添加更多的选项,与使用布尔值检查时相比。让我们探讨下面的一个例子,我们可以通过布尔检查和使用枚举类型声明来检查一个操作是否成功或失败。

class Result { 
  success: boolean; // in our code we can set this to either true or false 
  // also we must have seen constructs like `isSuccess` = true or false 
} 

//compared to using enums which are more descriptive of our intentions 
enum ResultStatus { FAILURE, SUCCESS } 

class enumResult { 
  status: ResultStatus; 
} 

为了创建一个类型,其值是枚举成员的键,我们可以利用keyoftypeof 方法。

enum Direction { 
  Up = "UP", 
  Down = "DOWN", 
  Left = "LEFT", 
  Right = "RIGHT", 
} 

 // this is same as 
 //type direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'; 
type direction = keyof typeof Direction; 

正如TypeScript文档所说,尽管枚举是在运行时存在的真实对象,但 keyof 关键字的工作方式与你对典型对象的预期不同

相反,使用keyof typeof 将得到一个将所有枚举键表示为字符串的类型,正如我们在上面看到的。

比较基于数字和字符串的枚举

基于数字的枚举和基于字符串的枚举之间的一个微妙区别是,对于基于数字的枚举,数字值成员有反向映射

反向映射允许我们检查一个给定的值在枚举的上下文中是否有效。为了更好地理解这一点,让我们看一下基于数字的枚举的编译输出。

//As we can see for number-based Enums, we could decide to leave off the initializers and the members autoincrements from Unauthorized -401 and so on -
enum StatusCodes { 
  OK = 200, 
  BadRequest = 400, 
  Unauthorized, 
  Forbidden, 
  NotFound, 
} 

//the transpiled JavaScript file is shown below (ignore the wrong status code ;)) 
var StatusCodes; 
(function (StatusCodes) { 
    StatusCodes[StatusCodes["OK"] = 200] = "OK"; 
    StatusCodes[StatusCodes["BadRequest"] = 400] = "BadRequest"; 
    StatusCodes[StatusCodes["Unauthorized"] = 401] = "Unauthorized"; 
    StatusCodes[StatusCodes["Forbidden"] = 402] = "Forbidden"; 
    StatusCodes[StatusCodes["NotFound"] = 403] = "NotFound"; 
})(StatusCodes || (StatusCodes = {})); 

如果我们看一下上面的第13-17 行,我们会发现我们可以通过它的键来解决一个值,而通过它的值来解决一个键。在line 13StatusCodes["OK"] = 200 is also equal to StatusCodes[200] = "OK"

不幸的是,这并不适用于基于字符串的枚举。相反,在基于字符串的枚举中,我们可以直接给枚举成员分配字符串值。让我们看看下面的转译输出。

enum Direction { 
  Up = "UP", 
  Down = "DOWN", 
  Left = "LEFT", 
  Right = "RIGHT", 
}  

//the transpiled .js file is shown below - 
var Direction; 
(function (Direction) { 
    Direction["Up"] = "UP"; 
    Direction["Down"] = "DOWN"; 
    Direction["Left"] = "LEFT"; 
    Direction["Right"] = "RIGHT"; 
})(Direction || (Direction = {})); 

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

//this will show direction '1' 
console.log(Direction.Down);  

// this will show message 'Down' as string representation of enum member 
console.log(Direction[Direction.Down]); 

字符串枚举可以很容易地从它们的索引中推断出来,或者通过查看它们第一个成员的初始化器。枚举上的自动初始化值是其属性名称。要从一个字符串值中获得一个枚举成员的数字值,你可以使用这个。

const directionStr = "Down"; 
// this will show direction '1' 
console.log(Direction[directionStr]); 

我们可以使用这些值来进行简单的字符串比较,例如:。

// Sample code to check if a user presses "UP" on say a game console 
const stringEntered = "UP" 

if (stringEntered === Direction.Up){ 
    console.log('The user has pressed key UP'); 
    console.log(stringEntered); //"UP" 
}

正如我们前面提到的,记录数字枚举的成员不是那么有用,因为我们看到的只是数字。在使用这些枚举类型时,还有一个松散类型的问题,因为静态允许的值不仅是枚举成员的值,任何数字都可以接受。很奇怪,对吗?请看下面的插图。

const enum LogLevel { 
  ERROR, 
  WARN, 
  INFO, 
  DEBUG, 
} 

function logger(log: LogLevel) { 
  return 'different error types'  
} 
console.log(logger(LogLevel.ERROR)) // "different error types"  
console.log(logger(12)) // "different error types"  

运行时和编译时的枚举

基于常量的枚举

使用基于常量的枚举,我们可以避免由TS编译器生成的代码,这在访问枚举值时很有用。基于常量的枚举在运行时没有一个表示。相反,其成员的值被直接使用。

除了使用const 关键字外,基于常量的枚举的定义与普通枚举一样。唯一的区别在于它们的行为和用法。让我们看看下面的一个例子。

const enum test  { 
  A, 
  B, 
} 

//usage  
function testConst(val: test) { 
   if(test.A) { 
     return "A" 
   }  
   if(test.B) { 
     return "B" 
   } else { 
     return "Undefined" 
   } 
} 

基于常量的枚举只能使用常量枚举表达式,并且在编译过程中不被添加,这与常规枚举声明不同。常量枚举成员在使用地点也是被内联的。因此,这就推断出常量枚举不能有计算的成员。在编译之后,它们被表示成如下所示。

console.log(testConst) 

function testConst(val) { 
    if (0 /* A */) { 
        return "A"; 
    } 
    if (1 /* B */) { 
        return "B"; 
    } 
}  

从上面我们可以看到,枚举成员的值是由TS内联的。另外,常量枚举不允许反向查找,它的行为与普通的JavaScript对象一样。

枚举成员也可以成为类型。这适用于某些枚举成员只有其对应的值的情况。我们还应该注意到,为了在不考虑常量枚举的情况下发出映射代码,我们可以在tsconfig.json 文件中打开preserveConstEnums 编译器选项。

如果我们在TS配置文件中设置了preserveConstEnums 选项后再次编译我们的代码,编译器仍将为我们内联代码,但它也会发出映射代码,当一段JavaScript代码需要访问它时,这就变得很有用了。

遗留支持的最佳实践

在现代TypeScript中,有一种方法是实现基于字符串的枚举,以便它们在JavaScript环境中向后兼容。这样做的目的是希望当ECMAScript最终在JavaScript中实现枚举时,我们可以利用这种语法。这就是遗留支持的作用。

为了在TypeScript中做到这一点,我们可以使用as const 方法和一个普通的对象。让我们看看下面的一个例子。

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

然而,为了在我们的代码中使用这种方法来获得键,我们需要使用keyof typeof 方法。请看下面的一个例子。

type Dir = typeof Direction[keyof typeof Direction]; 

总结

字符串枚举很灵活,我们既可以把键值作为枚举来添加,也可以只添加键值,如果键值是相同的,并且我们不关心枚举的大小写敏感性的话。在TypeScript中使用字符串枚举时,我们不一定需要知道每个枚举值包含的确切字符串。

同样重要的是要指出,虽然枚举可以与字符串和数字成员混合(异构),但不清楚为什么你会想这样做。

枚举成员的值可以是常量的,也可以是计算的,正如我们前面所讨论的。对于常量枚举,枚举必须是第一个成员,并且没有初始化器。另外,它们的值可以在编译时计算或被计算。如果你有兴趣,TypeScript文档包含了关于常量枚举和计算枚举的更多细节

使用TypeScript这样的类型化语言并使用这样的功能的好处是,像Visual Studio Code这样流行的IDE可以帮助我们通过自动完成从值列表中选择枚举值。当我们的任务是在枚举的值之间进行比较时,甚至在它们的常规使用中,这一点特别有用。

TypeScript字符串枚举,以及何时和如何使用它们》一文出现在LogRocket博客上。