专栏首页全栈修仙之路在 TS 中如何处理特殊值

在 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<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

五、参考资源

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 遇到这些 TS 问题你会头晕么?

    相信很多读者看到 let value: Fonum = 12; 这一行,TS 编译器并未提示任何错误会感到惊讶。很明显数字 12 并不是 Fonum 枚举的成员...

    阿宝哥
  • TypeScript 可辨识联合类型

    TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守...

    阿宝哥
  • TypeScript 交叉类型

    TypeScript 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

    阿宝哥
  • 嗯,春招两次腾讯面试都挂二面了,分享下我失败+傻傻的面试经历

    这个春招估计也要介绍了吧,自己投的公司也不多吧,投简历的时候,如果你提前批和正常网申都投的话,可能会获得两次笔试/面试的机会,我投了两次腾讯,不过,两次都在二面...

    乔戈里
  • 嗯,春招两次腾讯面试都挂二面了,分享下我失败+傻傻的面试经历

    这个春招估计也要介绍了吧,自己投的公司也不多吧,投简历的时候,如果你提前批和正常网申都投的话,可能会获得两次笔试/面试的机会,我投了两次腾讯,不过,两次都在二面...

    帅地
  • 地哥的腾讯面试经历

    腾讯提前批的面试应该是一个月前就开始的,我第一个投的公司就是腾讯了,人生的第一次笔试和面试也献给了腾讯。先说下笔试吧,笔试是 5 道编程题,个人觉得,腾讯的笔试...

    Java3y
  • Kubernetes集群使用网络存储NFS

    NFS即网络文件系统Network File System,它是一种分布式文件系统协议,最初是由Sun MicroSystems公司开发的类Unix操作系统之上...

    小小科
  • 基本类型_TypeScript笔记2

    JavaScript有7种类型:Boolean、Number、String、Undefined、Null、Object,以及ES6新增的Symbol

    ayqy贾杰
  • 泛型就这么简单

    Java3y
  • 雷军强推:小米造最强超分辨率算法,现已开源

    本篇是基于 NAS 的图像超分辨率的文章,知名学术性自媒体 Paperweekly 在该文公布后迅速跟进,发表分析称「属于目前很火的 AutoML / Neur...

    机器之心

扫码关注云+社区

领取腾讯云代金券