理解类型的一种方式是将其视为值的集合。有时值分为两个级别
在本章中,我们将研究如何向基本级别类型添加特殊值。
添加特殊值的一种方法是创建一个新类型,它是基本类型的超集,其中一些值是特殊的。这些特殊值被称为 哨兵值。它们存在于 带内(想想在同一个通道内),作为普通值的兄弟姐妹。
例如,考虑以下可读流的接口
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
} }