上一章探討 TypeScript 列舉的運作方式。在本章中,我們將探討列舉的替代方案。
列舉會將成員名稱對應到成員值。如果我們不需要或不想要間接引用,可以使用所謂的原始文字類型聯集,每個值一個。在深入探討細節之前,我們需要了解原始文字類型。
快速回顧:我們可以將類型視為值的集合。
單例類型是具有單一元素的類型。原始文字類型是單例類型
type UndefinedLiteralType = undefined;
type NullLiteralType = null;
type BooleanLiteralType = true;
type NumericLiteralType = 123;
type BigIntLiteralType = 123n; // --target must be ES2020+
type StringLiteralType = 'abc';
UndefinedLiteralType
是具有單一元素 undefined
的類型,依此類推。
重要的是要知道這裡有兩個語言層次(我們已經在本書前面遇到這些層次)。考慮以下變數宣告
: 'abc' = 'abc'; const abc
'abc'
代表一個類型(字串文字類型)。'abc'
代表一個值。原始文字類型的兩個使用案例是
字串參數的重載,這使得以下方法呼叫的第一個參數可以決定第二個參數的類型
.addEventListener('click', myEventHandler); elem
我們可以使用原始文字類型的聯集,透過列舉其成員來定義類型
type IceCreamFlavor = 'vanilla' | 'chocolate' | 'strawberry';
繼續閱讀以取得有關第二個使用案例的更多資訊。
我們將從列舉開始,並將其轉換為字串文字類型的聯集。
enum NoYesEnum {= 'No',
No = 'Yes',
Yes
}function toGerman1(value: NoYesEnum): string {
switch (value) {
.No:
case NoYesEnum'Nein';
return .Yes:
case NoYesEnum'Ja';
return
}
}.equal(toGerman1(NoYesEnum.No), 'Nein');
assert.equal(toGerman1(NoYesEnum.Yes), 'Ja'); assert
NoYesStrings
是 NoYesEnum
的聯集類型版本
type NoYesStrings = 'No' | 'Yes';
function toGerman2(value: NoYesStrings): string {
switch (value) {
'No':
case 'Nein';
return 'Yes':
case 'Ja';
return
}
}.equal(toGerman2('No'), 'Nein');
assert.equal(toGerman2('Yes'), 'Ja'); assert
類型 NoYesStrings
是字串文字類型 'No'
和 'Yes'
的聯集。聯集類型運算子 |
與集合論聯集運算子 ∪
相關。
下列程式碼示範窮盡檢查適用於字串文字類型的聯集
// @ts-expect-error: Function lacks ending return statement and
// return type does not include 'undefined'. (2366)
function toGerman3(value: NoYesStrings): string {
switch (value) {
'Yes':
case 'Ja';
return
} }
我們忘記了 'No'
的情況,而 TypeScript 會警告我們這個函式可能會傳回非字串的值。
我們也可以更明確地檢查窮盡
class UnsupportedValueError extends Error {constructor(value: never) {
super('Unsupported value: ' + value);
}
}
function toGerman4(value: NoYesStrings): string {
switch (value) {
'Yes':
case 'Ja';
return default:
// @ts-expect-error: Argument of type '"No"' is not
// assignable to parameter of type 'never'. (2345)
new UnsupportedValueError(value);
throw
} }
現在,如果 value
是 'No'
,TypeScript 會警告我們會執行 default
情況。
有關窮盡檢查的更多資訊
如需有關此主題的更多資訊,請參閱 §12.7.2.2「透過窮盡檢查防止遺漏情況」。
字串文字聯集的一個缺點是,非成員值可能會被誤認為是成員
type Spanish = 'no' | 'sí';
type English = 'no' | 'yes';
: Spanish = 'no';
const spanishWord: English = spanishWord; const englishWord
這是合乎邏輯的,因為西班牙文的 'no'
和英文的 'no'
是相同的值。實際的問題是,沒有辦法為它們提供不同的身分。
LogLevel
我們也可以使用符號單例類型的聯集,而不是字串文字類型的聯集。這次我們從不同的列舉開始
enum LogLevel {= 'off',
off = 'info',
info = 'warn',
warn = 'error',
error }
轉譯為符號單例類型的聯集,如下所示
= Symbol('off');
const off = Symbol('info');
const info = Symbol('warn');
const warn = Symbol('error');
const error
// %inferred-type: unique symbol | unique symbol |
// unique symbol | unique symbol
type LogLevel =
| typeof off
| typeof info
| typeof warn
| typeof error
;
為什麼我們需要 typeof
?off
等是值,無法出現在類型方程式中。類型運算子 typeof
會將值轉換為類型,修正此問題。
我們來考慮前一個範例的兩個變體。
我們可以內嵌符號(而不是參照個別的 const
宣告)嗎?唉,類型運算子 typeof
的運算元必須是識別碼或由點分隔的識別碼「路徑」。因此,這個語法是非法的
type LogLevel = typeof Symbol('off') | ···
let
取代 const
我們可以使用 let
取代 const
來宣告變數嗎?(這不一定是改進,但仍是一個有趣的問題。)
我們不能,因為我們需要 TypeScript 為 const
宣告的變數推斷出更狹窄的類型
// %inferred-type: unique symbol
= Symbol('constSymbol');
const constSymbol
// %inferred-type: symbol
= Symbol('letSymbol1'); let letSymbol1
使用 let
,LogLevel
僅會是 symbol
的別名。
const
斷言通常會解決這類問題。但它們在此情況下不起作用
// @ts-expect-error: A 'const' assertions can only be applied to references to enum
// members, or string, number, boolean, array, or object literals. (1355)
= Symbol('letSymbol2') as const; let letSymbol2
LogLevel
下列函式會將 LogLevel
的成員轉換為字串
function getName(logLevel: LogLevel): string {
switch (logLevel) {
:
case off'off';
return :
case info'info';
return :
case warn'warn';
return :
case error'error';
return
}
}
.equal(
assertgetName(warn), 'warn');
這兩種方法如何比較?
回想這個範例,其中西班牙文的 'no'
與英文的 'no'
混淆
type Spanish = 'no' | 'sí';
type English = 'no' | 'yes';
: Spanish = 'no';
const spanishWord: English = spanishWord; const englishWord
如果我們使用符號,我們就不會有這個問題
= Symbol('no');
const spanishNo = Symbol('sí');
const spanishSí type Spanish = typeof spanishNo | typeof spanishSí;
= Symbol('no');
const englishNo = Symbol('yes');
const englishYes type English = typeof englishNo | typeof englishYes;
: Spanish = spanishNo;
const spanishWord// @ts-expect-error: Type 'unique symbol' is not assignable to type 'English'. (2322)
: English = spanishNo; const englishWord
聯集類型與列舉有一些共通點
但它們也有所不同。符號單例類型的聯集的缺點是
符號單例類型的聯集的優點是
要了解它們如何運作,請考慮表示下列表達式的資料結構語法樹
1 + 2 + 3
語法樹為
後續步驟
這是語法樹的典型物件導向實作
// Abstract = can’t be instantiated via `new`
abstract class SyntaxTree1 {}
class NumberValue1 extends SyntaxTree1 {constructor(public numberValue: number) {
super();
}
}
class Addition1 extends SyntaxTree1 {constructor(public operand1: SyntaxTree1, public operand2: SyntaxTree1) {
super();
} }
SyntaxTree1
是 NumberValue1
和 Addition1
的超類別。關鍵字 public
是下列語法的語法糖
.numberValue
numberValue
初始化此屬性這是使用 SyntaxTree1
的範例
= new Addition1(
const tree new NumberValue1(1),
new Addition1(
new NumberValue1(2),
new NumberValue1(3), // trailing comma
, // trailing comma
); )
注意:引數清單中的尾隨逗號自 ECMAScript 2016 起在 JavaScript 中被允許。
如果我們透過聯合類型定義語法樹(A 行),我們不需要物件導向繼承
class NumberValue2 {constructor(public numberValue: number) {}
}
class Addition2 {constructor(public operand1: SyntaxTree2, public operand2: SyntaxTree2) {}
}type SyntaxTree2 = NumberValue2 | Addition2; // (A)
由於 NumberValue2
和 Addition2
沒有超類別,因此它們不需要在其建構函式中呼叫 super()
。
有趣的是,我們以與先前相同的方式建立樹
= new Addition2(
const tree new NumberValue2(1),
new Addition2(
new NumberValue2(2),
new NumberValue2(3),
,
); )
最後,我們了解區分聯合。以下是 SyntaxTree3
的類型定義
interface NumberValue3 {: 'number-value';
kind: number;
numberValue
}
interface Addition3 {: 'addition';
kind: SyntaxTree3;
operand1: SyntaxTree3;
operand2
}type SyntaxTree3 = NumberValue3 | Addition3;
我們已從類別切換到介面,因此從類別實例切換到純粹物件。
區分聯合的介面必須至少有一個共用屬性,而且每個介面中該屬性的值都必須不同。該屬性稱為判別式或標籤。SyntaxTree3
的判別式為 .kind
。其類型為字串文字類型。
比較
這是符合 SyntaxTree3
的物件
: SyntaxTree3 = { // (A)
const tree: 'addition',
kind: {
operand1: 'number-value',
kind: 1,
numberValue,
}: {
operand2: 'addition',
kind: {
operand1: 'number-value',
kind: 2,
numberValue,
}: {
operand2: 'number-value',
kind: 3,
numberValue,
}
}; }
我們不需要 A 行中的類型註解,但它有助於確保資料具有正確的結構。如果我們沒有在此處執行此操作,我們將在稍後發現問題。
在以下範例中,tree
的類型是區分聯合。每次我們檢查其判別式(C 行)時,TypeScript 會相應地更新其靜態類型
function getNumberValue(tree: SyntaxTree3) {
// %inferred-type: SyntaxTree3
; // (A)
tree
// @ts-expect-error: Property 'numberValue' does not exist on type 'SyntaxTree3'.
// Property 'numberValue' does not exist on type 'Addition3'.(2339)
.numberValue; // (B)
tree
if (tree.kind === 'number-value') { // (C)
// %inferred-type: NumberValue3
; // (D)
tree.numberValue; // OK!
return tree
};
return null }
在 A 行中,我們尚未檢查辨識符 .kind
。因此,tree
的目前類型仍然是 SyntaxTree3
,我們無法在 B 行中存取屬性 .numberValue
(因為聯合中只有一種類型具有此屬性)。
在 D 行中,TypeScript 知道 .kind
是 'number-value'
,因此可以為 tree
推斷類型 NumberValue3
。這就是為什麼這次在下一行中存取 .numberValue
是可以的。
我們以如何為辨識聯合實作函式的範例來結束這個步驟。
如果有一個運算可以套用在所有子類型的成員上,類別和辨識聯合的方法不同
以下範例示範函式方法。辨識符會在 A 行中檢查,並決定要執行哪兩個 switch
情況。
function syntaxTreeToString(tree: SyntaxTree3): string {
switch (tree.kind) { // (A)
'addition':
case syntaxTreeToString(tree.operand1)
return + ' + ' + syntaxTreeToString(tree.operand2);
'number-value':
case String(tree.numberValue);
return
}
}
.equal(syntaxTreeToString(tree), '1 + 2 + 3'); assert
請注意,TypeScript 會對辨識聯合執行窮盡性檢查:如果我們忘記一個情況,TypeScript 會警告我們。
這是前一個程式碼的物件導向版本
abstract class SyntaxTree1 {
// Abstract = enforce that all subclasses implement this method:
abstract toString(): string;
}
class NumberValue1 extends SyntaxTree1 {constructor(public numberValue: number) {
super();
}toString(): string {
String(this.numberValue);
return
}
}
class Addition1 extends SyntaxTree1 {constructor(public operand1: SyntaxTree1, public operand2: SyntaxTree1) {
super();
}toString(): string {
.operand1.toString() + ' + ' + this.operand2.toString();
return this
}
}
= new Addition1(
const tree new NumberValue1(1),
new Addition1(
new NumberValue1(2),
new NumberValue1(3),
,
);
)
.equal(tree.toString(), '1 + 2 + 3'); assert
每種方法都很好地執行一種可擴充性
使用物件導向方法時,如果我們要新增一個新的運算,我們必須修改每個類別。但是,新增一個新的類型不需要變更現有的程式碼。
使用函式方法時,如果我們要新增一個新的類型,我們必須修改每個函式。相反地,新增新的運算很簡單。
辨識聯合和一般聯合類型有兩點共通
接下來的兩個小節探討辨識聯合優於一般聯合的兩個優點
使用辨識聯合時,值會取得描述性屬性名稱。讓我們比較
一般聯合
type FileGenerator = (webPath: string) => string;
type FileSource1 = string|FileGenerator;
區分聯合
interface FileSourceFile {: 'FileSourceFile',
type: string,
nativePath
}
interface FileSourceGenerator {: 'FileSourceGenerator',
type: FileGenerator,
fileGenerator
}type FileSource2 = FileSourceFile | FileSourceGenerator;
現在閱讀原始碼的人會立即知道字串是什麼:本機路徑名稱。
以下區分聯合無法作為一般聯合實作,因為我們無法在 TypeScript 中區分聯合的類型。
interface TemperatureCelsius {: 'TemperatureCelsius',
type: number,
value
}
interface TemperatureFahrenheit {: 'TemperatureFahrenheit',
type: number,
value
}type Temperature = TemperatureCelsius | TemperatureFahrenheit;
以下實作列舉的模式在 JavaScript 中很常見
= {
const Color : Symbol('red'),
red: Symbol('green'),
green: Symbol('blue'),
blue; }
我們可以嘗試在 TypeScript 中如下使用
// %inferred-type: symbol
.red; // (A)
Color
// %inferred-type: symbol
type TColor2 = // (B)
| typeof Color.red
| typeof Color.green
| typeof Color.blue
;
function toGerman(color: TColor): string {
switch (color) {
.red:
case Color'rot';
return .green:
case Color'grün';
return .blue:
case Color'blau';
return default:
// No exhaustiveness check (inferred type is not `never`):
// %inferred-type: symbol
;
color
// Prevent static error for return type:
new Error();
throw
} }
唉,Color
的每個屬性的類型都是 symbol
(A 行),而 TColor
(B 行)是 symbol
的別名。因此,我們可以將任何符號傳遞給 toGerman()
,而 TypeScript 在編譯時不會抱怨
.equal(
asserttoGerman(Color.green), 'grün');
.throws(
assert=> toGerman(Symbol())); // no static error! ()
const
斷言通常有助於這種情況,但這次不行
= {
const ConstColor : Symbol('red'),
red: Symbol('green'),
green: Symbol('blue'),
blueas const;
}
// %inferred-type: symbol
.red; ConstColor
唯一解決這個問題的方法是透過常數
= Symbol('red');
const red = Symbol('green');
const green = Symbol('blue');
const blue
// %inferred-type: unique symbol
;
red
// %inferred-type: unique symbol | unique symbol | unique symbol
type TColor2 = typeof red | typeof green | typeof blue;
= {
const Color : 'red',
red: 'green',
green: 'blue',
blueas const; // (A)
}
// %inferred-type: "red"
.red;
Color
// %inferred-type: "red" | "green" | "blue"
type TColor =
| typeof Color.red
| typeof Color.green
| typeof Color.blue
;
我們需要在 A 行中使用 as const
,這樣 Color
的屬性就不會具有更通用的類型 string
。然後 TColor
也有比 string
更具體的類型。
與使用具有符號值屬性的物件作為列舉相比,具有字串值屬性的物件
優點
缺點
以下範例展示了 受 Java 啟發的列舉模式,它可以在純 JavaScript 和 TypeScript 中使用
class Color {= new Color();
static red = new Color();
static green = new Color();
static blue
}
// @ts-expect-error: Function lacks ending return statement and return type
// does not include 'undefined'. (2366)
function toGerman(color: Color): string { // (A)
switch (color) {
.red:
case Color'rot';
return .green:
case Color'grün';
return .blue:
case Color'blau';
return
}
}
.equal(toGerman(Color.blue), 'blau'); assert
唉,TypeScript 沒有執行詳盡性檢查,這就是為什麼我們在 A 行會收到錯誤訊息。
下表總結了 TypeScript 中列舉及其替代方案的特徵
獨特性 | 命名空間 | 迭代 | 編譯時成員檢查 | 執行時成員檢查 | 窮舉性 | |
---|---|---|---|---|---|---|
數字列舉 | - |
✔ |
✔ |
✔ |
- |
✔ |
字串列舉 | ✔ |
✔ |
✔ |
✔ |
- |
✔ |
字串共用體 | - |
- |
- |
✔ |
- |
✔ |
符號共用體 | ✔ |
- |
- |
✔ |
- |
✔ |
辨別共用體 | - (1) |
- |
- |
✔ |
- (2) |
✔ |
符號屬性 | ✔ |
✔ |
✔ |
- |
- |
- |
字串屬性 | - |
✔ |
✔ |
✔ |
- |
✔ |
列舉模式 | ✔ |
✔ |
✔ |
✔ |
✔ |
- |
表格欄位標題
instanceof
。表格儲存格中的腳註
TColor
的建議。