了解類型的其中一種方式是將其視為值集合。有時會有兩個層級的值
在本章中,我們將探討如何將特殊值新增至基本層級類型。
新增特殊值的一種方式是建立一個新類型,此類型是基本類型的超集,其中某些值為特殊值。這些特殊值稱為哨兵。它們存在於頻帶內(想像在同一個頻道內),作為一般值的同類項。
舉例來說,考量下列可讀取串流的介面
interface InputStream {getNextLine(): string;
}
目前,.getNextLine()
只處理文字列,但不處理檔案結尾 (EOF)。我們如何新增對 EOF 的支援?
可能性包括
.getNextLine()
之前需要呼叫的附加方法 .isEof()
。.getNextLine()
在到達 EOF 時擲回例外狀況。接下來的兩個小節說明我們可以引入哨兵值的兩種方式。
null
或 undefined
至類型在使用嚴格的 TypeScript 時,沒有任何簡單的物件類型(透過介面、物件模式、類別等定義)包含 null
。這使其成為一個良好的哨兵值,我們可以透過聯合類型將其新增至基本類型 string
type StreamValue = null | string;
interface InputStream {getNextLine(): StreamValue;
}
現在,每當我們使用 .getNextLine()
回傳的值時,TypeScript 會強制我們考量兩種可能性:字串和 null
– 例如
function countComments(is: InputStream) {
= 0;
let commentCount while (true) {
= is.getNextLine();
const line // @ts-expect-error: Object is possibly 'null'.(2531)
if (line.startsWith('#')) { // (A)
++;
commentCount
}if (line === null) break;
};
return commentCount }
在 A 行中,我們無法使用字串方法 .startsWith()
,因為 line
可能為 null
。我們可以透過下列方式修正此問題
function countComments(is: InputStream) {
= 0;
let commentCount while (true) {
= is.getNextLine();
const line if (line === null) break;
if (line.startsWith('#')) { // (A)
++;
commentCount
}
};
return commentCount }
現在,當執行到達 A 行時,我們可以確定 line
不為 null
。
我們也可以使用除了 null
以外的值作為哨兵。符號和物件最適合這個任務,因為它們每一個都有唯一的識別碼,不會有其他值被誤認為它。
以下是使用符號表示 EOF 的方法
= Symbol('EOF');
const EOF type StreamValue = typeof EOF | string;
為什麼我們需要 typeof
而不能直接使用 EOF
?那是因為 EOF
是值,而不是類型。類型運算子 typeof
會將 EOF
轉換為類型。有關值和類型的不同語言層級的詳細資訊,請參閱 §7.7「兩個語言層級:動態與靜態」。
如果方法可能會傳回任何值,我們該怎麼辦?我們要如何確保基本值和元值不會混淆?以下是一個可能發生混淆的範例
<T> {
interface InputStreamgetNextValue(): T;
}
無論我們為 EOF
選擇什麼值,都存在有人建立 InputStream<typeof EOF>
並將該值加入串流的風險。
解決方案是將一般值和特殊值分開,這樣它們就不會混淆。特殊值獨立存在稱為 頻段外(想像成不同的頻道)。
區分聯合 是聯合類型,包含多個物件類型,而這些物件類型至少有一個共通的屬性,稱為判別式。判別式必須對每個物件類型有不同的值,我們可以將它視為物件類型的 ID。
InputStreamValue
在以下範例中,InputStreamValue<T>
是區分聯合,而它的判別式是 .type
。
<T> {
interface NormalValue: 'normal'; // string literal type
type: T;
data
}
interface Eof {: 'eof'; // string literal type
type
}type InputStreamValue<T> = Eof | NormalValue<T>;
<T> {
interface InputStreamgetNextValue(): InputStreamValue<T>;
}
function countValues<T>(is: InputStream<T>, data: T) {
= 0;
let valueCount while (true) {
// %inferred-type: Eof | NormalValue<T>
= is.getNextValue(); // (A)
const value
if (value.type === 'eof') break;
// %inferred-type: NormalValue<T>
; // (B)
value
if (value.data === data) { // (C)
++;
valueCount
}
};
return valueCount }
最初,value
的類型是 InputStreamValue<T>
(A 行)。然後我們排除判別式 .type
的值 'eof'
,而它的類型縮小為 NormalValue<T>
(B 行)。這就是為什麼我們可以在 C 行存取屬性 .data
。
IteratorResult
在決定如何實作 反覆運算器 時,TC39 不想使用固定的哨兵值。否則,該值可能會出現在可反覆運算的物件中並中斷程式碼。一種解決方案是在開始反覆運算時選擇哨兵值。TC39 反而選擇使用具有共通屬性 .done
的區分聯合
<TYield> {
interface IteratorYieldResult?: false; // boolean literal type
done: TYield;
value
}
<TReturn> {
interface IteratorReturnResult: true; // boolean literal type
done: TReturn;
value
}
type IteratorResult<T, TReturn = any> =
| IteratorYieldResult<T>
| IteratorReturnResult<TReturn>;
其他類型的聯合類型可以像辨別聯合一樣方便,只要我們有辦法區分聯合的成員類型。
一種可能性是透過獨特屬性來區分成員類型
interface A {: number;
one: number;
two
}
interface B {: number;
three: number;
four
}type Union = A | B;
function func(x: Union) {
// @ts-expect-error: Property 'two' does not exist on type 'Union'.
// Property 'two' does not exist on type 'B'.(2339)
console.log(x.two); // error
if ('one' in x) { // discriminating check
console.log(x.two); // OK
} }
另一種可能性是透過 typeof
和/或實例檢查來區分成員類型
type Union = [string] | number;
function logHexValue(x: Union) {
if (Array.isArray(x)) { // discriminating check
console.log(x[0]); // OK
} else {console.log(x.toString(16)); // OK
} }