一文读懂TS 中联合类型和交叉类型各自的含义

目录
  • 一、简单的联合类型
  • 二、对象类型的并集和交集
  • 三、文氏图
  • 四、集合理论
  • 五、类型和集合之间的关系
  • 六、了解联合类型和交叉类型
  • 七、交叉类型的真实示例
  • 八、总结
  • 九、参考资源

联合类型在 TypeScript 中相当流行,你可能已经用过很多次了。交叉类型稍微不那么常见。它们似乎引起更多的困惑。

你有没有想过这些名字是怎么来的?虽然你可能对两种类型的并集有一些直观感受,但交集通常不太容易理解。

阅读本文之后,你将对这些类型有更好的了解,这将使你在代码中使用它们时更有信心。

一、简单的联合类型

联合类型通常与 nullundefined 一起使用:

const sayHello = (name: string | undefined) => { /* ... */ };

例如,这里 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给sayHello 函数。

sayHello("semlinker");
sayHello(undefined);

查看这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。

二、对象类型的并集和交集

这种直觉也适用于复杂类型。

interface Foo {
  foo: string;
  name: string;
}

interface Bar {
  bar: string;
  name: string;
}

const sayHello = (obj: Foo | Bar) => { /* ... */ };

sayHello({ foo: "foo", name: "lolo" });
sayHello({ bar: "bar", name: "growth" });

Foo | Bar 是含有 FooBar 所有必须属性的类型。在 sayHello 内部只能访问 obj.name,因为它是两种类型都包含的唯一属性。

那么 FooBar 类型的交集又怎么样?

const sayHello = (obj: Foo & Bar) => { /* ... */ };

sayHello({ foo: "foo", bar: "bar", name: "kakuqo" });

现在 sayHello 要求 obj 参数同时包含 foobar 的属性。所以在 sayHello 内部,有可能同时访问obj.fooobj.barobj.name

嗯,但是它有什么交集呢?有人可能会说,因为 obj 同时具有 Foo 和 Bar 的属性,所以它听起来更像是属性的并集,而不是交集。类似地,两个对象类型联合将得到一个类型,该类型只含有组成类型的属性的交集。

三、文氏图

文氏图(英语:Venn diagram),或译 Venn 图、温氏图、维恩图等,是在集合论(或者类的理论)数学分支中,在不太严格的意义下用以表示集合(或类)的一种草图。它们用于展示在不同的事物群组(集合)之间的数学或逻辑联系,尤其适合用来表示集合(或)类之间的 “大致关系”,它也常常被用来帮助推导(或理解推导过程)关于集合运算(或类运算)的一些规律。

在文氏图法中,如果有论域,则以一个矩形框(的内部区域)表示论域;各个集合(或类)就以圆/椭圆(的内部区域)来表示。两个圆/椭圆相交,其相交部分表示两个集合(或类)的公共元素,两个圆/椭圆不相交(相离或相切,而实际上在文氏图中相切是没有什么意义的,因为文氏图是以图形的内部区域来表示的)则说明这两个集合(或类)没有公共元素。

比如黄色的圆圈(集合 A)可以表示两足的所有动物。蓝色的圆圈(集合 B)可以表示会飞的所有动物。黄色和蓝色的圆圈交叠的区域(叫做交集)包含会飞且两足的所有动物 —— 比如鹦鹉。(把每个单独的动物类型想像为在这个图中的某个点)。

(图片来源: https://zh.wikipedia.org/wiki/文氏 图)

集合 A 和 B 的组合区域叫做集合 A 和 B 的并集。在这个示例中并集包含要么两足、要么会飞、要么两足并且会飞的所有东西。圆圈交叠暗示着两个集合的交集非空 —— 就是说在事实上有动物同时在黄色和蓝色圆圈中。

需要注意的是,文氏图与其它的图示法一样,它不能准确表示一个集合(或类)中到底有哪些元素。

四、集合理论

你还记得数学课中称为集合的概念吗?在数学中,集合是对象(例如数字)的集合。例如 {1, 2, 7} 是一组。所有正数也可以形成一组(无限个)。

可以将集合合并在一起(并集)。{1, 2}{4, 5} 的并集是 {1, 2, 4, 5}

集合也可以交叉。两个集合的交集是一个集合,它只包含两个集合中出现的那些数字。因此,{1, 2, 3}{3, 4, 5} 的交集是 {3}

下面我们来换一种思考方式。假设有四个集合:红色的东西,蓝色的东西,大的东西,和小的东西。

如果你把所有红色的东西和所有小的东西的集合相交,你就得到了属性的并集 —— 集合里的所有东西都有红色的属性和小的属性。

但如果取红色小物体与蓝色小物体的并集,则结果集中只有小(small)属性是普遍存在的。“red small” 与  “blue small” 相交产生 “small”。

换句话说,取值域的并集会产生一组交叉的属性,反之亦然。具体过程如下图所示:

(图片来源: https://stackoverflow.com/questions/38855908/naming-of-typescripts-union-and-interp-types)

五、类型和集合之间的关系

计算机科学和数学在许多地方都有重叠。这样的地方之一就是类型系统。

从数学角度看,一种类型是该类型所有可能值的集合。例如,string 类型是所有可能的字符串的集合:{'a', 'b', 'ab', ...}。当然,这是一个无限的集合。同样,number 类型是一组所有可能的数字的集合:{1, 2, 3, 4, ...}

类型 undefined 是一个仅包含单个值的集合:{ undefined },该类型在 TypeScript 中被称为单元类型。

那么对象类型(比如接口)呢?类型 Foo 是包含 foo 和 name 属性的所有对象的集合。

六、了解联合类型和交叉类型

有了这些知识,你现在就可以了解联合和交叉类型的含义了。

联合类型 A | B 表示一个集合,该集合是与类型A关联的一组值和与类型 B 关联的一组值的并集。交叉类型 A & B 表示一个集合,该集合是与类型 A 关联的一组值和与类型 B 关联的一组值的交集。

因此,Foo | Bar 表示有 foo 和 name 属性的对象集和有 bar 和 name 属性的对象集的并集。属于这类集合的对象都含有 name 属性。有些有 foo 属性,有些有 bar 属性。

Foo & Bar 表示具有 foo 和 name 属性的对象集和具有 bar 和 name 属性的对象集的交集。换句话说,集合包含了属于由 Foo 和 Bar 表示的集合的对象。只有具有这三个属性(foo、bar 和 name)的对象才属于交集。

继续阅读:TypeScript 交叉类型

七、交叉类型的真实示例

联合类型非常普遍,所以让我们关注一个交叉类型的例子。

在 React 中,当你声明一个类组件时,可以使用它的属性类型对其进行参数化:

class Counter extends Component<CounterProps> { /* ... */ }

在类中,你可以通过 this.props 访问属性。然而, this.props 的类型不只是 CounterProps,而是:

Readonly<CounterProps> & Readonly<{ children?: ReactNode; }>

这样做的原因是 React 组件可以接收子元素:

<Counter><span>Hello Semlinker</span></Counter>

通过 children 属性可以访问到子元素。this.props 的类型反映了这一点。它是(readonly)CounterProps 和含有可选的 children 属性的(readonly)对象类型交集。

在集合方面,它是含有 CounterProps 中定义的属性的对象集和与含有可选 children 属性的对象集的交集。结果是一组含有 CounterProps 所有属性和可选 children 属性的对象集。

八、总结

本文为了帮助读者更好地理解 TypeScript 中的联合类型和交叉类型,我们引入了文氏图、集合理论及类型和集合之间的关系这些内容。计算机科学和数学在许多地方都有重叠,理解数学相关的基本原理后可以使你更好地掌握编程概念。

九、参考资源

  • wiki - 文氏图
  • The-meaning-of-union-and-interp-types
  • naming-of-typescripts-union-and-interp-types
(0)

相关推荐

  • react常见的ts类型实践解析

    目录 正文 在dom节点上的类型 传入子组件的方法 !非空断言,一定有该方法或者属性 一个doms上的类型 导入一个组件需要的多个类型 选择一个组件的指定的几个属性 总结 正文 ts在中在react用处很是很广泛,本文是一篇关于在react中常用的类型总结,希望能带来一些思考和帮助. 一个函数组件 import React from "react"; type Props = { } const Header: React.FC<Props> = (props) =>

  • 记VUE3+TS获取组件类型的方法踩坑及解决

    目录 VUE3+TS获取组件类型的方法踩坑 遇到的坑 问题原因 解决办法 VUE3+TS获取组件ref实例 如何获取组件的类型呢? 总结 VUE3+TS获取组件类型的方法踩坑 获取组件类型的方法 const AccountRef = ref<InstanceType<typeof LoginAccount>>() 遇到的坑 typeof LoginAccount一直报红线提示错误 LoginAction: () => vo...' provides no match for

  • ts中的void和never类型及区别

    目录 ts中的void和never类型 void never 补充:void类型和never类型 void类型 never类型 ts中的void和never类型 void 表示没有任何类型 // 没有返回值的函数,其返回值类型为 void function warnUser(): void { console.log("void"); } // 申明为 void 类型的变量,只能赋予 undefined 和 null let unusable: void = undefined; 可以

  • vue3+ts中ref与reactive指定类型实现示例

    目录 ref 的基础特性 如何在ref中指定类型 reactive isRef.isReactive toRef.toRefs.toRaw ref 的基础特性 ref 约等于 reactive({ value: x }) ref() 可以定义时无参数,第一次赋值任意类型,然后就不能增加属性 const refa = ref(6) const rcta = reactive({ value: 12 }) console.log('refa:', refa) //RefImpl{...} conso

  • 一文读懂JAVA中HttpURLConnection的用法

    针对JDK中的URLConnection连接Servlet的问题,网上有虽然有所涉及,但是只是说明了某一个或几个问题,是以FAQ的方式来解决的,而且比较零散,现在对这个类的使用就本人在项目中的使用经验做如下总结: 1:> URL请求的类别: 分为二类,GET与POST请求.二者的区别在于: a:) get请求可以获取静态页面,也可以把参数放在URL字串后面,传递给servlet, b:) post与get的不同之处在于post的参数不是放在URL字串里面,而是放在http请求的正文内. 2:>

  • 一文读懂ava中的Volatile关键字使用

    在本文中,我们会介绍java中的一个关键字volatile. volatile的中文意思是易挥发的,不稳定的.那么在java中使用是什么意思呢? 我们知道,在java中,每个线程都会有个自己的内存空间,我们称之为working memory.这个空间会缓存一些变量的信息,从而提升程序的性能.当执行完某个操作之后,thread会将更新后的变量更新到主缓存中,以供其他线程读写. 因为变量存在working memory和main memory两个地方,那么就有可能出现不一致的情况. 那么我们就可以使

  • 一文读懂go中semaphore(信号量)源码

    运行时信号量机制 semaphore 前言 最近在看源码,发现好多地方用到了这个semaphore. 本文是在go version go1.13.15 darwin/amd64上进行的 作用是什么 下面是官方的描述 // Semaphore implementation exposed to Go. // Intended use is provide a sleep and wakeup // primitive that can be used in the contended case /

  • 一文读懂C++中的继承之菱形继承(案例分析)

    前言 我们上一篇说了世间万物都有一个继承体制,或多或少子类继承了父类的某些特征,但大多都是单向继承,但是就有些特例他就是多继承,比如: 我们从图片中就可以看到,两栖动物它既继承了水生动物的一部分特性,也继承了陆地动物的一些特性,那么我们的代码,会不会也会有这种多继承现象呢,我们一起来看一下. 提示:以下是本篇文章正文内容,下面案例可供参考 一.什么是多继承? 1.单继承 我们来看一个图先了解一下单继承,再看有什莫区别 也就是说,一个子类只有一个直接父类时称这个继承关系为单继承 2.多继承 我们把

  • 一文读懂C++中指针和内存分配

    指针 指针是保存内存位置地址的变量.我们知道声明的所有变量在内存中都有一个特定的地址.声明一个指针变量来指向内存中的这些地址. 声明指针变量的一般语法是: int p, *ptr; //声明变量p和指针变量ptr p = 4; //赋值4给变量p ptr = &p; //将p的地址分配给指针变量ptr 在内存中,这些声明将表示如下: 这是指针在内存中的内部表示.当地址变量分配给指针变量时,它指向的变量如上图所示. 由于 ptr具有变量 p 的地址,*ptr 将给出变量 p 的值(指针变量 ptr

  • 一文读懂ES7中的javascript修饰器

    什么是修饰器 修饰器(Decorator)是ES7的一个提案,它的出现能解决两个问题: 不同类间共享方法 编译期对类和方法的行为进行改变 用法也很简单,就是在类或方法的上面加一个@符,在vue in typescript中经常用到 以上的两个用处可能不太明白,没关系,我们开始第一个例子 例子1:修饰类 @setProp class User {} function setProp(target) { target.age = 30 } console.log(User.age) 这个例子要表达的

  • 一文读懂JavaScript 中的延迟加载属性模式

    传统上,开发人员在 JavaScript 类中为实例中可能需要的任何数据创建属性.对于在构造函数中随时可用的小块数据来说,这不是问题.但是,如果在实例中可用之前需要计算某些数据,您可能不想预先支付该费用.例如,考虑这个类: class MyClass { constructor() { this.data = someExpensiveComputation(); } } 在这里,data属性是作为执行一些昂贵计算的结果而创建的.如果您不确定是否会使用该属性,则预先执行该计算可能效率不高.幸运的

  • 一文读懂MySQL 表分区

    目录 1. 什么是表分区 2. 分区的两种方式 2.1 水平切分 2.2 垂直切分 3. 为什么需要表分区 4. 分区实践 4.1 RANGE 分区 4.2 LIST 分区 4.3 HASH 分区 4.4 KEY 分区 4.5 COLUMNS 分区 5. 常见分区命令 6. 小结 松哥之前写过文章跟大家介绍过用 MyCat 实现 MySQL 的分库分表,不知道有没有小伙伴研究过,MySQL 其实也自带了分区功能,我们可以创建一个带有分区的表,而且不需要借助任何外部工具,今天我们就一起来看看. 1

  • 一文读懂c++之static关键字

    一.静态变量 与C语言一样,可以使用static说明自动变量.根据定义的位置不同,分为静态全局变量和静态局部变量. 全局变量是指在所有花括号之外声明的变量,其作用域范围是全局可见的,即在整个项目文件内都有效.使用static修饰的全局变量是静态全局变量,其作用域有所限制,仅在定义该变量的源文件内有效,项目中的其他源文件中不能使用它. 块内定义的变量是局部变量,从定义之处开始到本块结束处为止是局部变量的作用域.使用static修饰的局部变量是静态局部变量,即定义在块中的静态变量.静态局部变量具有局

  • 一文读懂c++11 Lambda表达式

    1.简介 1.1定义 C++11新增了很多特性,Lambda表达式(Lambda expression)就是其中之一,很多语言都提供了 Lambda 表达式,如 Python,Java ,C#等.本质上, Lambda 表达式是一个可调用的代码单元[1]^{[1]}[1].实际上是一个闭包(closure),类似于一个匿名函数,拥有捕获所在作用域中变量的能力,能够将函数做为对象一样使用,通常用来实现回调函数.代理等功能.Lambda表达式是函数式编程的基础,C++11引入了Lambda则弥补了C

随机推荐