理解类型的一种方式是将其视为值的集合。有时值分为两个级别
在本章中,我们将研究如何向基本级别类型添加特殊值。
添加特殊值的一种方法是创建一个新类型,它是基本类型的超集,其中一些值是特殊的。这些特殊值被称为 哨兵值。它们存在于 带内(想想在同一个通道内),作为普通值的兄弟姐妹。
例如,考虑以下可读流的接口
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) {
let commentCount = 0;
while (true) {
const line = is.getNextLine();
// @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) {
let commentCount = 0;
while (true) {
const line = is.getNextLine();
if (line === null) break;
if (line.startsWith('#')) { // (A)
commentCount++;
}
}
return commentCount;
}现在,当执行到 A 行时,我们可以确定 line 不为 null。
我们还可以使用 null 以外的值作为哨兵。符号和对象最适合此任务,因为它们中的每一个都具有唯一的标识,并且没有其他值可以与其混淆。
以下是使用符号表示 EOF 的方法
const EOF = Symbol('EOF');
type StreamValue = typeof EOF | string;为什么我们需要 typeof 而不能直接使用 EOF?这是因为 EOF 是一个值,而不是一个类型。类型运算符 typeof 将 EOF 转换为类型。有关值和类型的不同语言级别的更多信息,请参阅 §7.7 “两个语言级别:动态与静态”。
如果一个方法可能返回 任何 值,我们该怎么办?我们如何确保基本值和元值不会混淆?这是一个可能发生这种情况的例子
interface InputStream<T> {
getNextValue(): T;
}无论我们为 EOF 选择什么值,都存在有人创建 InputStream<typeof EOF> 并将该值添加到流中的风险。
解决方案是将普通值和特殊值分开,这样它们就不会混淆。单独存在的特殊值称为 带外(想想不同的通道)。
可辨识联合类型 是多个对象类型的联合类型,所有这些类型至少有一个共同的属性,即所谓的 鉴别器。鉴别器对于每个对象类型必须具有不同的值——我们可以将其视为对象类型的 ID。
InputStreamValue在以下示例中,InputStreamValue<T> 是一个可辨识联合类型,其鉴别器为 .type。
interface NormalValue<T> {
type: 'normal'; // string literal type
data: T;
}
interface Eof {
type: 'eof'; // string literal type
}
type InputStreamValue<T> = Eof | NormalValue<T>;
interface InputStream<T> {
getNextValue(): InputStreamValue<T>;
}function countValues<T>(is: InputStream<T>, data: T) {
let valueCount = 0;
while (true) {
// %inferred-type: Eof | NormalValue<T>
const value = is.getNextValue(); // (A)
if (value.type === 'eof') break;
// %inferred-type: NormalValue<T>
value; // (B)
if (value.data === data) { // (C)
valueCount++;
}
}
return valueCount;
}最初,value 的类型为 InputStreamValue<T>(A 行)。然后我们排除了鉴别器 .type 的值 'eof',并且其类型被缩小为 NormalValue<T>(B 行)。这就是我们可以在 C 行访问属性 .data 的原因。
IteratorResult在决定如何实现 迭代器 时,TC39 不想使用固定的哨兵值。否则,该值可能会出现在可迭代对象中并破坏代码。一种解决方案是在开始迭代时选择一个哨兵值。TC39 选择使用具有公共属性 .done 的可辨识联合类型
interface IteratorYieldResult<TYield> {
done?: false; // boolean literal type
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true; // boolean literal type
value: TReturn;
}
type IteratorResult<T, TReturn = any> =
| IteratorYieldResult<T>
| IteratorReturnResult<TReturn>;只要我们有办法区分联合的成员类型,其他类型的联合类型就可以像可辨识联合类型一样方便。
一种可能性是通过唯一属性来区分成员类型
interface A {
one: number;
two: number;
}
interface B {
three: number;
four: number;
}
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
}
}