理解Ts联合类型和交叉类型

10,026

写在前面

最近写了个需求,当前存在两个列表数据,一个是提问列表,一个是回答列表。两个列表数据存在共同的数据结构:头像、名称、时间。于是我们根据数据后台的数据接口字段提炼出以下的ts类型。


/**单个提问数据结构 */
interface QsData {
  avator?: string
  time?: number | string
  name?: string
  quesitons: {
    title: string
    picture: string[]
  }
}

/**单个回答数据结构 */
interface AsData {
  avator?: string
  time?: number | string
  name?: string
  answers: {
    text: string
    audio?: {
      url: string
      total: number
    }
  }
} 

类型拆分解耦

很明显,优秀的代码根据DNR(do not repeat)原则,相同重复的声明片段是不好的。可以根据功能和模块拆分以上重复接口声明

// 头像接口类型
export interface DataAvator {
  avator?: string
  time?: number | string
  name?: string
}

// 提问数据接口类型
export interface Qs {
  quesitons: {
    title: string
    picture: string[]
  }
}

// 问题接口数据类型
export interface As {
  answers: {
    text: string
    audio?: {
      url: string
      total: number
    }
  }
}

接口混入

但是我们需要的声明是刚开始那种复杂的数据类型,现在我们需要把简单的数据类型组装回去,怎么优雅的组装回去呢这是个问题?于是乎我想到了工具类型,借用工具类我们可以快速从简单的类型得到复杂的想要的类型。之前写过一篇走进工具泛型也有提及这块的ts知识。好了,我们现在可以来构造一个混入的ts工具类。

type Mixin<T, X> = {
  [P in keyof (T & X)]: (T & X)[P]
};

它也可以更简单

type Mixin<T, X> = T & X;

我们使用Mixin去组装复杂的列表数据如下:

// 用泛型混入,方便之后还会出现什么数据也带有头像等信息
// 方便做拓展和复用
type MixWithDataAvator<T> = Mixin<DataAvator, T>;

// 组装Qs和As 拼接成想要的复杂数据类型
export interface AskConfig {
  asker: MixWithDataAvator<Qs>
  answer?: MixWithDataAvator<As>
}

// 最终输出列表数组类型
export type AskConfigList = AskConfig[];

到这里为止一切都很顺利,相比之前我们一开始定义的两个数据声明已经非常松散了。不管是维护哪一部分的声明都不需要改动特别大。

ts: & 和 |

不得不说,这个前言真的特别长,现在才引出&和|符号。回到上面的Mixin部分,之所以能有这样的解决方案全靠了&符号。当我们把类型用&连接起来就是ts交叉类型,用|连接起来就是联合类型。起初笔者对这两个概念都不甚清晰,于是趁着项目中有用之处仔细捋了一捋。
这两个类型概念可以用交集和并集的思维来理解。(PS:非常重要!先抛开数学集合中的交集和并集概念,网上大多的文章话不多说直接摆出数学集合中的概念其实是一种误导,因为跟以下的类集合完全不是一码事)。我们来看下图: 先看左边的类型交集:
存在类型1:蓝色物体 类型2:小型物体,当两类型发生交集,那么交集出来的类型就是蓝色并且小型的物体。所以,交叉类型的理解就是类型交集产生出的新的类型,并且这个类型包含参与交集的类型的所有属性。
再看右边的类型并集:
存在类型1:蓝色物体 类型2:红色物体,当两类型发生并集,联合类型的理解就是类型并集之后产生的一个垃圾桶,所有参与并集的类型都像垃圾桶中的垃圾,当我们使用联合类型的时候,就像垃圾桶里捡垃圾,要么捡起来的是蓝色物体,要么就是红色物体。
总结:&交叉类型:产生一个包含所有属性的新类型。 |联合类型:产生一个包含所有类型的选择集类型。

联合类型

interface A {
  name: string
  age: number
}

interface B {
  name: number
  id: string
}

type Union = A | B;
const c: Union

当我们使用联合类型赋值的时候,数据结构只能选择满足形如A或者形如B
当我们使用联合类型读取属性的时候,只能获取其共同的属性名 ps(idea 语法提示)。如果访问的是非共同的属性,必须做好类型保护以防止bug。

交叉类型

当我们使用交叉类型读取属性,可以获取所有类型的所有属性名,赋值的时候需要满足所有类型的结构

注意,非常重要,当我们交叉的类型中含有相同属性名但属性类型不一样的情况,该属性会成为never类型

原因也很简单:交叉操作,name不可能同时是number类型又是string类型,所以成为了never类型。

never类型

Never:即不存在的值的类型,按照上面的例子就是说,不存在属性name的值即是number又是string类型。也比较好理解。never类型也有使用场景,但我们在书写譬如if else / switch的多代码流的情况下对never类型赋值能及时抛出语法异常帮我们避免case遗漏的情况。想更加详细的了解never类型如何帮助代码检查这块 可以阅读这篇文章Never类型

总结

自个儿在网上搜索了许多文章看了许多复杂的解释,把自个儿理解的东西以更简单的方式梳理下来,2021年第一天返工也是第一篇文章~。但愿对一些同样对这两个概念还有疑惑的小伙伴有些许帮助吧。