在 TS 中如何处理特殊值
创建了一个“重学TypeScript”的微信群,想加群的小伙伴,加我微信 “semlinker”
,备注重学TS。
一、添加特殊的值
添加特殊值的一种方法是创建一个新类型,该类型是一些特殊值的基本类型的超集,这些特殊值称为哨兵。
举个示例,请考虑以下可读流接口:
interface InputStream { getNextLine(): string; }
目前, getNextLine
仅能处理文本行,而不能处理文件结尾(EOF)。那我们如何增加对 EOF 的支持呢?
有以下几种可选方案:
-
在调用
getNextLine()
方法前需调用一个额外的isEof()
方法。 -
当遇到 EOF 标志的时候,
getNextLine()
方法抛出一个异常。 - 为 EOF 设置一个哨兵值。
接下来我们将介绍引入特殊值的两种方式。
1.1 添加 null 或 undefined 到类型中
在 TypeScript 中 null
是一个很好的哨兵值,我们可以通过类型联合将其对应的 null 类型添加到新的类型中:
// 这里的null类型也称为单元类型 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-ignore: 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; // 判断为null,则跳出循环 if (line.startsWith('#')) { // (A) commentCount++; } } return commentCount; }
现在,当执行到 A 行时,我们可以确定此时 line
的值不是 null
,因此可以放心的调用字符串上的 startsWith
方法。
1.2 添加 symbol 到类型中
我们可以使用 null
以外的值作为哨兵。Symbols 和 objects 最适合这个任务,因为它们中的每个值都有唯一的标识,不会与其它值混淆起来。
下面我们使用 symbol 来表示 EOF:
const EOF = Symbol('EOF'); type StreamValue = typeof EOF | string;
需要注意的是,这里我们需要使用 typeof 操作符。TypeScript 是严格区分值和类型的:
- EOF(End Of File)是一个值。
- 联合类型操作符 | 的第一个操作数必须是类型。
另外对于前面定义的 InputStream 接口来说,为了让 getNextValue 方法的返回值更通用,我们可以使用泛型变量声明该方法的返回值类型:
interface InputStream{ getNextValue(): T; }
无论我们为了 EOF
想出什么特殊的值,总是可以使用 typeof EOF
来设置类型参数 T 的值。
1.3 单元类型
在 TypeScript 中还存在一种特殊的类型叫字面量类型,也被称为单元类型。该类型用于表示单个值的集合,典型的代表就是 null
和 undefined
类型。需要注意的是,字面量类型看起来像值,但它们实际上是类型。比如:
type A = 'A'; type StreamValue = 123 | string;
在以上示例中,字面量类型 123
看起来像一个值,但实际上它是一个类型(仅包含 123 的单元类型)。当然我们也可以使用另一种更直观的方式,即通过 typeof 操作符来获取变量的类型:
const EOF = 123; type StreamValue = typeof EOF | string;
单元类型是表示单个值的集合,那么在 TypeScript 中空集对应的类型是什么呢?相信大多数读者已经知道答案了,即 never
类型。因为它的域是空的,所以没有值可以赋给一个具有 never 类型的变量:
const x: never = "semlinker"; // Type '"semlinker"' is not assignable to type 'never'.
二、可辨识联合类型
可辨识联合类型是指多个对象类型至少含有一个通用的属性。对于每个对象类型,该属性必须具有不同的值 —— 我们可以将其视为对象类型的 ID。在下面的示例中, InputStreamValue
是可辨识的。
interface NormalValue{ type: 'normal'; data: T; } interface Eof { type: 'eof'; // End Of File } type InputStreamValue = Eof | NormalValue ; interface InputStream { getNextValue(): InputStreamValue ; } function countValues (is: InputStream , data: T) { let valueCount = 0; while (true) { const value = is.getNextValue(); if (value.type === 'eof') break; // (A) if (value.data === data) { // (B) valueCount++; } } return valueCount; }
由于在 A 行中已经进行了检查,所以在 B 行中我们能够访问 value 变量的 data
属性,该属性只存在于 NormalValue 类型的变量中。
三、迭代器的结果
在决定如何实现迭代器时,TC39 也不能使用固定的哨兵值。因为该值可能会出现在可迭代项和中断代码中。一种解决方案是在开始迭代时选择哨兵值。TC39 最终采用了包含一个公共属性 done 的可辨识联合:
interface IteratorYieldResult{ done?: false; value: TYield; } interface IteratorReturnResult { done: true; value: TReturn; } type IteratorResult = | IteratorYieldResult | IteratorReturnResult ;
四、其他类型的联合
只要我们能够区分联合类型的成员,那么其它的联合类型也可以作为可辨识联合类型。其中一种方案是通过独特的属性来区分:
interface A { one: number; two: number; } interface B { three: number; four: number; } type Union = A | B; function func(x: Union) { //@ts-ignore: Property 'two' does not exist on type 'Union'. // Property 'two' does not exist on type 'B'.(2339) console.log(x.two); if ('one' in x) { console.log(x.two); // OK } }
另一种方案是通过 typeof
或实例检查来区分:
type Union = [string] | number; function logHexValue(x: Union) { if (Array.isArray(x)) { console.log(x[0]); // OK } else { console.log(x.toString(16)); // OK } }
在实际开发中,联合类型的应用很广,但使用的过程中要特别注意,要做好类型保护,否则在运行时可能会导致出现严重的异常。对 TS 类型保护感兴趣的小伙伴,可以阅读一下 “在 TS 中如何实现类型保护?类型谓词了解一下”
这篇文章。
本文主要参考了“德国阮一峰” —— Axel Rauschmayer
大神的 special-values-typescript 这篇文章,感兴趣的小伙伴可阅读原文哟。