const
列舉
本章節將回答以下兩個問題
在 下一章節 中,我們將探討列舉的替代方案。
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
列舉為了觀察這個效應,我們先來檢視以下非 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
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
案例來擲回例外狀況。我們可以採取另一項措施。以下程式碼執行窮舉檢查:如果我們忘記考慮所有列舉成員,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
,因為我們永遠不會到達那裡。但是,如果我們新增成員 .Maybe
到 NoYes
,那麼 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
對本章節的回饋。