目前已出重學 TS 專題(二十四篇),歡迎感興趣的小夥關注全棧修仙之路一塊兒「重學TS」(掃描文末二維碼)。html
在 JavaScript 中布爾類型的變量含有有限範圍的值,即 true
和 false
。而在 TypeScript 中使用枚舉,你也能夠自定義類似的類型。git
這是一個枚舉的簡單示例:github
enum NoYes {
No,
Yes,
}
複製代碼
No
和 Yes
被稱爲枚舉 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), '是');
複製代碼
每一個枚舉成員都有一個 name 和一個 value。數字枚舉成員值的默認類型是 number 類型。也就是說,每一個成員的值都是一個數字:typescript
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]
);
複製代碼
常量的命名有幾種約定:session
Number.MAX_VALUE
;Symbol.asyncIterator
;NoYes
枚舉。與 JavaScript 對象相似,咱們可使用方括號來引用包含非法字符的枚舉成員:dom
enum HttpRequestField {
'Accept',
'Accept-Charset',
'Accept-Datetime',
'Accept-Encoding',
'Accept-Language',
}
assert.equal(HttpRequestField['Accept-Charset'], 1);
複製代碼
除了數字枚舉,咱們還可使用字符串做爲枚舉成員值:async
enum NoYes {
No = 'No',
Yes = 'Yes',
}
assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');
複製代碼
對於純字符串枚舉,咱們不能省略任何初始化程序。
最後一種枚舉稱爲異構枚舉。異構枚舉的成員值是數字和字符串的混合:
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 區分了三種指定枚舉成員值的方式:
若是枚舉只有字面量成員,咱們能夠將這些成員用做類型(相似於數字字面量能夠用做類型的方式):
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
以後添加相關提示,解釋忽略了什麼錯誤。請注意,這個註釋僅會隱藏報錯,而且咱們建議你少使用這一註釋。
若是能夠在編譯時計算枚舉成員的值,則該枚舉成員是常量。所以,咱們能夠隱式指定其值(即,讓 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,
}
複製代碼
若是枚舉僅含有常量成員,則不能再將成員用做類型。可是咱們仍然能夠進行完整性檢查。
能夠經過任意表達式設置枚舉成員的值。例如:
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(''),
}
複製代碼
在輸出數字枚舉的成員時,咱們只會看到數字:
enum NoYes { No, Yes }
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 0
// 1
複製代碼
將枚舉用做類型時,容許的值不僅是枚舉成員的值 – 能夠接受任何數字:
enum NoYes { No, Yes }
function func(noYes: NoYes) {}
func(33); // no error!
複製代碼
爲何沒有更嚴格的靜態檢查?Daniel Rosenwasser解釋:
該行爲是由按位運算引發的。有時
SomeFlag.Foo | SomeFlag.Bar
打算產生另外一種SomeFlag
。相反,您最終獲得了number
,而且你不想回退到SomeFlag
。我認爲,若是咱們再次運行 TypeScript 以後仍然有枚舉,那麼咱們將爲位標誌創建一個單獨的構造。
個人建議是使用字符串枚舉:
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');
複製代碼
在 Node.js 文件系統模塊中,幾個函數具備參數模式。它的值用於經過 Unix 保留的編碼來指定文件權限:
這意味着權限能夠用 9 位表示(3 個類別,每一個類別具備 3 個權限):
用戶 | 組 | 全部 | |
---|---|---|---|
權限 | r,w,x | r,w,x | r,w,x |
位 | 八、七、6 | 5 4 3 | 2 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。
位模式背後的主要思想是存在一組標誌,而且能夠選擇這些標誌的任何子集。所以,使用 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!');
複製代碼
有時,咱們有一組屬於同類型的常量:
// 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 會執行靜態檢查。當使用布爾值表示替代方案時,枚舉一般是一種更具自我描述性的選擇。
例如,爲了表示列表是否有序,咱們可使用布爾值:
class List1 {
isOrdered: boolean;
// ···
}
複製代碼
可是,枚舉更具備自我描述性,並具備其餘好處,即若是須要,咱們能夠在之後添加更多選擇項。
enum ListKind { ordered, unordered }
class List2 {
listKind: ListKind;
// ···
}
複製代碼
一樣,咱們能夠經過布爾值或枚舉來表示操做是成功仍是失敗:
class Result1 {
success: boolean;
// ···
}
enum ResultStatus { failure, success }
class Result2 {
status: ResultStatus;
// ···
}
複製代碼
考慮如下建立正則表達式的函數。
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";
複製代碼
有兩組賦值操做:
給定一個數字枚舉:
enum NoYes {
No,
Yes,
}
複製代碼
普通的映射是從成員名稱到成員值:
// 靜態查找
assert.equal(NoYes.Yes, 1);
// 動態查找
assert.equal(NoYes['Yes'], 1);
複製代碼
數字枚舉還支持從成員值到成員名稱的反向映射:
assert.equal(NoYes[1], 'Yes');
複製代碼
基於字符串的枚舉在運行時具備更簡單的表示形式。
考慮如下枚舉:
enum NoYes {
No = 'NO!',
Yes = 'YES!',
}
複製代碼
它會被編譯爲如下 JavaScript 代碼:
var NoYes;
(function (NoYes) {
NoYes["No"] = "NO!";
NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));
複製代碼
TypeScript 不支持基於字符串枚舉的反向映射。
若是枚舉以 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 '是';
}
}
複製代碼
這與前面的代碼基本一致,可是使用了 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 '是';
}
}
複製代碼
TypeScript 將(非 const)枚舉視爲對象:
enum NoYes {
No = 'No',
Yes = 'Yes',
}
function func(obj: { No: string }) {
return obj.No;
}
assert.equal(
func(NoYes),
'No');
複製代碼
當咱們接受一個枚舉成員值時,咱們一般要確保:
在如下代碼中,咱們針對非法值採起了兩種措施:
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
分支會拋出異常。咱們能夠再採起一種措施。如下代碼執行全面性檢查:若是咱們忘記考慮全部枚舉成員,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
類型。可是,若是咱們添加一個成員 Maybe
到 NoYes
枚舉中,以後 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);
}
}
複製代碼
另外,若是咱們爲如下 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
語句。
咱們可使用 keyof
類型運算符建立類型,其元素是枚舉成員的 key。當咱們這樣作,咱們須要結合 keyof
和 typeof
一塊兒使用:
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
類型更方便。
若是使用 keyof
不使用 typeof
,則會獲得另外一個不太有用的類型:
type Keys = keyof HttpRequestKeyEnum;
// = 'toString' | 'toFixed' | 'toExponential' |
// 'toPrecision' | 'valueOf' | 'toLocaleString'
複製代碼
keyof HttpRequestKeyEnum
的結果與 keyof number
相同。
本文主要參考了「德國阮一峯」 —— Axel Rauschmayer 大神的 numeric-enums 這篇文章,感興趣的小夥伴可閱讀原文喲。 2ality.com/2020/01/typ…
建立了一個 「重學TypeScript」 的微信羣,想加羣的小夥伴,加我微信 "semlinker",備註重學TS。