const
枚举keyof
和枚举本章回答以下两个问题
在 下一章 中,我们将介绍枚举的替代方案。
boolean
是一种具有有限数量值的类型:false
和 true
。使用枚举,TypeScript 允许我们自己定义类似的类型。
这是一个数字枚举
enum NoYes {= 0,
No = 1, // trailing comma
Yes
}
.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1); assert
说明
No
和 Yes
称为枚举 NoYes
的_成员_。No
,值为 0
。我们可以像使用 true
、123
或 'abc'
等字面量一样使用成员,例如
function toGerman(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return
}
}.equal(toGerman(NoYes.No), 'Nein');
assert.equal(toGerman(NoYes.Yes), 'Ja'); assert
我们也可以使用字符串作为枚举成员值,而不是数字
enum NoYes {= 'No',
No = 'Yes',
Yes
}
.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes'); assert
最后一种枚举称为_异构枚举_。异构枚举的成员值是数字和字符串的混合
enum Enum {= 'One',
One = 'Two',
Two = 3,
Three = 4,
Four
}.deepEqual(
assert.One, Enum.Two, Enum.Three, Enum.Four],
[Enum'One', 'Two', 3, 4]
[; )
异构枚举不常用,因为它们的应用很少。
遗憾的是,TypeScript 仅支持数字和字符串作为枚举成员值。其他值,例如符号,是不允许的。
我们可以在两种情况下省略初始化器
这是一个没有任何初始化器的数字枚举
enum NoYes {,
No,
Yes
}.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1); assert
这是一个省略了一些初始化器的异构枚举
enum Enum {,
A,
B= 'C',
C = 'D',
D = 8, // (A)
E ,
F
}.deepEqual(
assert.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
[Enum0, 1, 'C', 'D', 8, 9]
[; )
请注意,我们不能省略 A 行中的初始化器,因为前一个成员的值不是数字。
命名常量(在枚举或其他地方)有几种先例
Number.MAX_VALUE
Math.SQRT2
Symbol.asyncIterator
NoYes
枚举中使用了它。与 JavaScript 对象类似,我们可以引用枚举成员的名称
enum HttpRequestField {'Accept',
'Accept-Charset',
'Accept-Datetime',
'Accept-Encoding',
'Accept-Language',
}.equal(HttpRequestField['Accept-Charset'], 1); assert
无法计算枚举成员的名称。对象字面量通过方括号支持计算属性键。
TypeScript 根据枚举成员的初始化方式将其分为三种
_字面量枚举成员_
_常量枚举成员_通过可以在编译时计算结果的表达式初始化。
_计算枚举成员_通过任意表达式初始化。
到目前为止,我们只使用了字面量成员。
在前面的列表中,较早提到的成员灵活性较低,但支持更多功能。继续阅读以获取更多信息。
如果枚举成员的值是指定的,则该成员是_字面量_
如果枚举只有字面量成员,我们可以将这些成员用作类型(类似于例如数字字面量可以用作类型)
enum NoYes {= 'No',
No = 'Yes',
Yes
}function func(x: NoYes.No) { // (A)
;
return x
}
func(NoYes.No); // OK
// @ts-expect-error: Argument of type '"No"' is not assignable to
// parameter of type 'NoYes.No'.
func('No');
// @ts-expect-error: Argument of type 'NoYes.Yes' is not assignable to
// parameter of type 'NoYes.No'.
func(NoYes.Yes);
A 行中的 NoYes.No
是_枚举成员类型_。
此外,字面量枚举支持穷尽性检查(我们将在稍后介绍)。
如果枚举成员的值可以在编译时计算,则该成员是常量。因此,我们可以隐式指定其值(即,我们让 TypeScript 为我们指定)。或者我们可以显式指定它,并且只允许使用以下语法
+
、-
、~
+
、-
、*
、/
、%
、<<
、>>
、>>>
、&
、|
、^
这是一个其成员都是常量的枚举示例(我们将在后面看到该枚举是如何使用的)
enum Perm {= 1 << 8, // bit 8
UserRead = 1 << 7,
UserWrite = 1 << 6,
UserExecute = 1 << 5,
GroupRead = 1 << 4,
GroupWrite = 1 << 3,
GroupExecute = 1 << 2,
AllRead = 1 << 1,
AllWrite = 1 << 0,
AllExecute }
通常,常量成员不能用作类型。但是,仍然会执行穷尽性检查。
_计算枚举成员_的值可以通过任意表达式指定。例如
enum NoYesNum {= 123,
No = Math.random(), // OK
Yes }
这是一个数字枚举。基于字符串的枚举和异构枚举的限制更多。例如,我们不能使用方法调用来指定成员值
enum NoYesStr {= 'No',
No // @ts-expect-error: Computed values are not permitted in
// an enum with string valued members.
= ['Y', 'e', 's'].join(''),
Yes }
TypeScript 不对计算枚举成员执行穷尽性检查。
记录数字枚举的成员时,我们只看到数字
, Yes }
enum NoYes { No
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 0
// 1
使用枚举作为类型时,静态允许的值不仅是枚举成员的值,还接受任何数字
, Yes }
enum NoYes { Nofunction func(noYes: NoYes) {}
func(33); // no error!
为什么没有更严格的静态检查?Daniel Rosenwasser 解释说
这种行为是由按位运算引起的。有时
SomeFlag.Foo | SomeFlag.Bar
旨在生成另一个SomeFlag
。但最终得到的是number
,而您不想将其转换回SomeFlag
。我认为如果我们重新设计 TypeScript 并且仍然有枚举,我们会为位标志创建一个单独的结构。
稍后将更详细地演示如何将枚举用于位模式。
我的建议是优先使用基于字符串的枚举(为了简洁起见,本章并不总是遵循此建议)
='No', Yes='Yes' } enum NoYes { No
一方面,日志输出对人类更有用
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 'No'
// 'Yes'
另一方面,我们获得了更严格的类型检查
function func(noYes: NoYes) {}
// @ts-expect-error: Argument of type '"abc"' is not assignable
// to parameter of type 'NoYes'.
func('abc');
// @ts-expect-error: Argument of type '"Yes"' is not assignable
// to parameter of type 'NoYes'.
func('Yes'); // (A)
甚至不允许使用等于成员值的字符串(A 行)。
在 Node.js 文件系统模块 中,有几个函数具有参数 mode
。它通过从 Unix 继承的数字编码指定文件权限
这意味着权限可以用 9 位表示(3 个类别,每个类别 3 个权限)
用户 | 组 | 所有 | |
---|---|---|---|
权限 | r、w、x | r、w、x | r、w、x |
位 | 8, 7, 6 | 5, 4, 3 | 2, 1, 0 |
Node.js 没有这样做,但我们可以使用枚举来处理这些标志
enum Perm {= 1 << 8, // bit 8
UserRead = 1 << 7,
UserWrite = 1 << 6,
UserExecute = 1 << 5,
GroupRead = 1 << 4,
GroupWrite = 1 << 3,
GroupExecute = 1 << 2,
AllRead = 1 << 1,
AllWrite = 1 << 0,
AllExecute }
位模式通过 按位或 组合
// User can change, read and execute.
// Everyone else can only read and execute.
.equal(
assert.UserRead | Perm.UserWrite | Perm.UserExecute |
Perm.GroupRead | Perm.GroupExecute |
Perm.AllRead | Perm.AllExecute,
Perm0o755);
// User can read and write.
// Group members can read.
// Everyone can’t access at all.
.equal(
assert.UserRead | Perm.UserWrite | Perm.GroupRead,
Perm0o640);
位模式背后的主要思想是,有一组标志,并且可以选择这些标志的任何子集。
因此,使用真正的集合来选择子集是执行相同任务的更直接的方式
enum Perm {= 'UserRead',
UserRead = 'UserWrite',
UserWrite = 'UserExecute',
UserExecute = 'GroupRead',
GroupRead = 'GroupWrite',
GroupWrite = 'GroupExecute',
GroupExecute = 'AllRead',
AllRead = 'AllWrite',
AllWrite = 'AllExecute',
AllExecute
}function writeFileSync(
: string, permissions: Set<Perm>, content: string) {
thePath// ···
}writeFileSync(
'/tmp/hello.txt',
new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
'Hello!');
有时,我们有一些属于一起的常量集
= Symbol('off');
const off = Symbol('info');
const info = Symbol('warn');
const warn = Symbol('error'); const error
这是一个很好的枚举用例
enum LogLevel {= 'off',
off = 'info',
info = 'warn',
warn = 'error',
error }
枚举的一个好处是常量名称被分组并嵌套在命名空间 LogLevel
中。
另一个好处是我们自动为它们获取类型 LogLevel
。如果我们想要为常量使用这种类型,则需要做更多的工作
type LogLevel =
| typeof off
| typeof info
| typeof warn
| typeof error
;
有关此方法的更多信息,请参阅 §13.1.3 “符号单例类型的联合”。
当使用布尔值表示备选项时,枚举通常更具描述性。
例如,要表示列表是否有序,我们可以使用布尔值
class List1 {: boolean;
isOrdered// ···
}
但是,枚举更具描述性,并且具有如果需要,我们可以在以后添加更多备选项的额外好处。
, unordered }
enum ListKind { ordered
class List2 {: ListKind;
listKind// ···
}
同样,我们可以通过布尔值指定如何处理错误
function convertToHtml1(markdown: string, throwOnError: boolean) {
// ···
}
或者我们可以通过枚举值来做到这一点
enum ErrorHandling {= 'throwOnError',
throwOnError = 'showErrorsInContent',
showErrorsInContent
}function convertToHtml2(markdown: string, errorHandling: ErrorHandling) {
// ···
}
考虑以下创建正则表达式的函数。
= 'g';
const GLOBAL = '';
const NOT_GLOBAL type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;
function createRegExp(source: string,
: Globalness = NOT_GLOBAL) {
globalnessnew RegExp(source, 'u' + globalness);
return
}
.deepEqual(
assertcreateRegExp('abc', GLOBAL),
/abc/ug);
.deepEqual(
assertcreateRegExp('abc', 'g'), // OK
/abc/ug);
我们可以使用枚举来代替字符串常量
enum Globalness {= 'g',
Global = '',
notGlobal
}
function createRegExp(source: string, globalness = Globalness.notGlobal) {
new RegExp(source, 'u' + globalness);
return
}
.deepEqual(
assertcreateRegExp('abc', Globalness.Global),
/abc/ug);
.deepEqual(
assert// @ts-expect-error: Argument of type '"g"' is not assignable to parameter of type 'Globalness | undefined'. (2345)
createRegExp('abc', 'g'), // error
/abc/ug);
这种方法有什么好处?
Globalness
只接受成员名称,不接受字符串。TypeScript 将枚举编译为 JavaScript 对象。例如,以下枚举
enum NoYes {,
No,
Yes }
TypeScript 将此枚举编译为
var NoYes;
function (NoYes) {
("No"] = 0] = "No";
NoYes[NoYes["Yes"] = 1] = "Yes";
NoYes[NoYes[|| (NoYes = {})); })(NoYes
在此代码中,进行了以下赋值
"No"] = 0;
NoYes["Yes"] = 1;
NoYes[
0] = "No";
NoYes[1] = "Yes"; NoYes[
有两组赋值
给定一个数字枚举
enum NoYes {,
No,
Yes }
正常映射是从成员名称到成员值
// Static (= fixed) lookup:
.equal(NoYes.Yes, 1);
assert
// Dynamic lookup:
.equal(NoYes['Yes'], 1); assert
数字枚举还支持从成员值到成员名称的_反向映射_
.equal(NoYes[1], 'Yes'); assert
反向映射的一个用例是打印枚举成员的名称。
function getQualifiedName(value: NoYes) {
'NoYes.' + NoYes[value];
return
}.equal(
assertgetQualifiedName(NoYes.Yes), 'NoYes.Yes');
字符串枚举在运行时具有更简单的表示。
考虑以下枚举。
enum NoYes {= 'NO!',
No = 'YES!',
Yes }
它被编译成以下 JavaScript 代码。
var NoYes;
function (NoYes) {
("No"] = "NO!";
NoYes["Yes"] = "YES!";
NoYes[|| (NoYes = {})); })(NoYes
TypeScript 不支持字符串枚举的反向映射。
const
枚举如果枚举以关键字 const
为前缀,则它在运行时没有表示形式。相反,直接使用其成员的值。
为了观察这种效果,让我们首先检查以下非 const 枚举。
enum NoYes {= 'No',
No = 'Yes',
Yes
}
function toGerman(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return
} }
TypeScript 将此代码编译为:
"use strict";
var NoYes;
function (NoYes) {
("No"] = "No";
NoYes["Yes"] = "Yes";
NoYes[|| (NoYes = {}));
})(NoYes
function toGerman(value) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
} }
这与之前的代码相同,但现在枚举是 const。
const enum NoYes {,
No,
Yes
}function toGerman(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return
} }
现在,枚举作为构造函数的表示形式消失了,只剩下其成员的值。
function toGerman(value) {
switch (value) {
case "No" /* No */:
return 'Nein';
case "Yes" /* Yes */:
return 'Ja';
} }
TypeScript 将(非 const)枚举视为对象。
enum NoYes {= 'No',
No = 'Yes',
Yes
}function func(obj: { No: string }) {
.No;
return obj
}.equal(
assertfunc(NoYes), // allowed statically!
'No');
当我们接受枚举成员值时,我们通常要确保:
继续阅读以获取更多信息。我们将使用以下枚举。
enum NoYes {= 'No',
No = 'Yes',
Yes }
在以下代码中,我们采取了两项措施来防止非法值。
function toGerman1(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return default:
new TypeError('Unsupported value: ' + JSON.stringify(value));
throw
}
}
.throws(
assert// @ts-expect-error: Argument of type '"Maybe"' is not assignable to
// parameter of type 'NoYes'.
=> toGerman1('Maybe'),
() /^TypeError: Unsupported value: "Maybe"$/);
这些措施是:
NoYes
会阻止将非法值传递给参数 value
。default
case 抛出异常。我们可以采取一项措施。以下代码执行_穷举检查_:如果我们忘记考虑所有枚举成员,TypeScript 会向我们发出警告。
class UnsupportedValueError extends Error {constructor(value: never) {
super('Unsupported value: ' + value);
}
}
function toGerman2(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return default:
new UnsupportedValueError(value);
throw
} }
穷举检查是如何工作的?对于每种情况,TypeScript 都会推断 value
的类型。
function toGerman2b(value: NoYes) {
switch (value) {
.No:
case NoYes// %inferred-type: NoYes.No
;
value'Nein';
return .Yes:
case NoYes// %inferred-type: NoYes.Yes
;
value'Ja';
return default:
// %inferred-type: never
;
valuenew UnsupportedValueError(value);
throw
} }
在默认情况下,TypeScript 会将 value
的类型推断为 never
,因为我们永远不会到达那里。但是,如果我们向 NoYes
添加一个成员 .Maybe
,则 value
的推断类型为 NoYes.Maybe
。并且该类型在静态上与 new UnsupportedValueError()
的参数类型 never
不兼容。这就是为什么我们在编译时会收到以下错误消息。
Argument of type 'NoYes.Maybe' is not assignable to parameter of type 'never'.
方便的是,这种穷举检查也适用于 if
语句。
function toGerman3(value: NoYes) {
if (value === NoYes.No) {
'Nein';
return if (value === NoYes.Yes) {
} else 'Ja';
return
} else {new UnsupportedValueError(value);
throw
} }
或者,如果我们指定了返回类型,我们也会得到穷举性检查。
function toGerman4(value: NoYes): string {
switch (value) {
.No:
case NoYes: NoYes.No = value;
const x'Nein';
return .Yes:
case NoYes: NoYes.Yes = value;
const y'Ja';
return
} }
如果我们向 NoYes
添加一个成员,则 TypeScript 会抱怨 toGerman4()
可能会返回 undefined
。
这种方法的缺点
if
语句(更多信息)。keyof
和枚举我们可以使用 keyof
类型运算符来创建其元素为枚举成员键的类型。当我们这样做时,我们需要将 keyof
与 typeof
结合起来。
enum HttpRequestKeyEnum {'Accept',
'Accept-Charset',
'Accept-Datetime',
'Accept-Encoding',
'Accept-Language',
}// %inferred-type: "Accept" | "Accept-Charset" | "Accept-Datetime" |
// "Accept-Encoding" | "Accept-Language"
type HttpRequestKey = keyof typeof HttpRequestKeyEnum;
function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
// ···
}
typeof
使用 keyof
如果我们在不使用 typeof
的情况下使用 keyof
,我们会得到一个不同的、不太有用的类型。
// %inferred-type: "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"
type Keys = keyof HttpRequestKeyEnum;
keyof HttpRequestKeyEnum
与 keyof number
相同。
@spira_mirabilis
对本章的反馈。