9

在 TS 中如何处理特殊值

 4 years ago
source link: https://semlinker.com/ts-special-value/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

创建了一个“重学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<T> {
  getNextValue(): T;
}

无论我们为了 EOF 想出什么特殊的值,总是可以使用 typeof EOF 来设置类型参数 T 的值。

1.3 单元类型

在 TypeScript 中还存在一种特殊的类型叫字面量类型,也被称为单元类型。该类型用于表示单个值的集合,典型的代表就是 nullundefined 类型。需要注意的是,字面量类型看起来像值,但它们实际上是类型。比如:

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<T> {
  type: 'normal';
  data: T;
}

interface Eof { 
  type: 'eof'; // End Of File
}

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) {
    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<TYield> {
  done?: false;
  value: TYield;
}

interface IteratorReturnResult<TReturn> {
  done: true;
  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-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 这篇文章,感兴趣的小伙伴可阅读原文哟。

https://2ality.com/2020/01/special-values-typescript.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK