从集合的角度来看 Typescript 的类型

1,039 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

前言

在学习 Typescript 的过程当中,不可避免的会见到各种各样的 Typescript 类型,如果不去了解 Typescript 类型是什么,那么在 Typescript 的学习路上一定是寸步难行。本文会通过数学中的 集合 知识来介绍一下什么是 Typescript 的类型。

什么是 TypeScript

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

我们从一张图来看一下 TypeScript 和 JavaScript 之间的关系。

image.png

集合的概念

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

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

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

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

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

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

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

image.png

(图片来源: stackoverflow.com/questions/3…)

什么是 TypeScript 类型

TypeScript 是带有类型语法的 JavaScript,它是⼀种建⽴在 JavaScript 基础上的强类型编程语⾔。它内置了常⻅的基础类型,⽐如 string、number 和 boolean 等类型。

在这些基础的帮助下,我们可以在声明变量的同时,为它声明类型。

let name: string = 'xiaoming'

那么在 TypeScript 中,什么是类型呢?我们可以把所有的类型比喻为数学中的集合,并且这个集合,是保存着一些拥有特定的特性的值。

关于基本类型

比如在 Typescript 中,number 就是所有数字的集合,string 类型是全部字符串的集合,在集合中最小的集合就是空集,也就对应了 TypeScript 中的 never 类型,关于 nerver 类型是怎么出现的,以及它的作用,后面在详细说明。

然后我们之前做题的时候有碰到过第二小的集合也就是字面量类型,又称为单元类型(Unit Type),通过 JavaScript 中的 const 关键字,我们就能够将一个变量声明为字面量类型,简单的理解就是:通过 const 关键字声明的变量是无法改变的,它是一个常量,并且称作字面量类型;而通过 let 关键字声明的变量是可以改变的,所以它就会被 Typescript 根据声明的值赋予一个基础类型。

从下面这张图来看一下他们之间的关系:

image.png

关于对象

并且对于对象来说,越多的属性约束,就会不断缩小查找它的范围,比方说你去找一个数,第一个条件是它是一个自然数,第二个条件是它是一个奇数,第三个条件是它小于10,经过你的每一次添加约束,就会缩小查找的范围。

image.png

那么对象也是一样的,假如有一个 person 对象,它需要有一个 name 属性,那么就可以缩小一圈范围,只需要属性存在 name 的对象,在然后需要有一个 age 属性,或者在多一个 sex 属性,就可以不断地缩小查找范围,这就是类型对于对象的一个约束。

image.png

那么在理解了 TypeScript 中的类型可以通过集合的概念来理解之后,我们来细讲一下 TypeScript 中一些特殊的类型。

交叉类型

交叉类型的概念对应了集合中的交集的概念,也就是两个集合共有的部分作为他们的交集,比如在数学中集合 4>x>1 和集合 5>x>3 它们之间的并集就是 4>x>3:

image.png

这也对应了 数学符号中 & 的作用,并且 TypeScript 的联合类型中,也是使用 & 来表示联合类型的,那么看一下下面这个代码

let a: number
let b: string
a & b // nerver

应该就很好理解,一个 number 类型和 一个 string 类型的交集,一个变量既要是 number 类型又要是 string 类型,很明显是不可能的,所以他们的交叉类型为 nerver

但是要注意的一点,对于对象来说,我们要站在类型的角度上来看,它们都是对象类型,比如说 一个 a 对象和 b 对象:

let a = {
    name:'aa'
}
let b = {
    age: 18
}

这个时候的 a & b 是一个新的对象,对象里面需要既有 name 又有 age,这也就对应了上面关于对象类型的介绍,我们求两个对象的交叉类型,相当于说这个新的对象既要有 name 属性,又要有 age 属性,这样才能够满足 a 和 b 的交叉。

用一句话来表示就是:交叉类型是通过使用&符号,将多个类型合并为一个类型

这意味着我们可以把之前说的类型,多个叠加在一起作为一种类型

举例一个官方的例子🌰:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

class Person {
    constructor(public name: string) { }
    aaa(){}
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
jim.aa();

注意在这个例子当中,我们在一个函数中,为 result 通过交叉类型限定了它的属性,并且把传入的两个对象的属性分别拷贝到它的身上,这就是一个简单的创建混入的例子。

可以结合上面的 a 和 b 的例子做一个理解,一个新的对象 result 既要有 T 的属性,又要有 U 的属性,那么就只能在里面加上他们两个都有的属性。

那么接下来让我们来看一下和交叉类型相对应的联合类型。

联合类型

基本概念

联合类型的概念对应了集合中的并集的概念,也就是两个集合覆盖的部分都包括在并集之中,比如在数学中集合 3>=x>1 和集合 5>x>3 它们之间的并集就是 5>x>1:

image.png

这也对应了 数学符号中 | 的作用,那么在 TypeScript 的联合类型中,也是使用 | 来表示联合类型的,比如

let a: number | string = 1
a = 'string'
console.log(a) 
// 输出 string

代表这个 a 既可以是数组也可以是字符串,哪怕之后将它更改为字符串,Typescript也不会报错。

注意的踩坑点

关于联合类型用来组合两个对象的时候,需要注意有一个比较特殊的地方,假设我们现在定义两个对象接口:

interface Bird { 
    fly(); 
    layEggs(); 
} 
interface Fish { 
    swim(); 
    layEggs(); 
} 

他们有着公共的属性以及不同的属性,然后在你用联合类型来把它们两个做一个关联的时候:

function getSmallPet(): Fish | Bird { 
    // ... 
} 
let pet = getSmallPet(); 
pet.layEggs(); // okay 
pet.swim(); // errors

你会发现你只能使用它们公有的属性,没办法访问它们各自独立拥有的属性。

关于这点其实也很好理解,如果一个值的类型是 A | B,我们能够 确定 的是它包含了 A 和 B中共有的成员。

这个例子里, Bird具有一个 fly成员。 我们不能确定一个 Bird | Fish类型的变量是否有 fly方法。 如果变量在运行时是 Fish类型,那么调用 pet.fly()就出错了。

以上就是使用联合类型需要注意的一个关键点,在对于两个对象的联合类型中,就不能只是简单地用并集来进行理解了。

nerver 类型

nerver 类型的出现

上面提到过了 nerver 类型对应了集合中的空集,那么它是怎么出现的呢,比如说一个类型,我们要它既是 1 又是 2,那么它就只能收窄为 nerver,我们可以定义一个变量:

type result = 1 & 2 // 结果为never

这有点类似于集合中的空集的概念,空集是任何集合的子集,所以当这个类型无法实现的时候,它就会返回 nerver

nerver 类型的作用

那么 nerver 在实际的编程中有什么作用呢?

在编程当中,并不是所有的函数都能够做到完美的,对于一些函数如何处理不合法的输入才能保证类型安全,可以通过 异常 来进行处理,但是 异常 带来的问题问题就是一方面破坏了引用透明性,另一方面导致非本地跳转影响了后续的控制流分析。

nerver 可以用来使异常处理更加的安全,如果一个函数返回了 nerver 类型,那就意味着这个函数不会返回给调用者,这就意味着在调用一个返回值为 nerver 的函数后,调用的代码就成了 unreachable code,这样就可以做 unreachable code 分析了

举一个小例子🌰:

type Foo = string | number; 

function controlFlowAnalysisWithNever(foo: Foo) { 
    if (typeof foo === "string") { 
        // 这里 foo 被收窄为 string 类型 
    } 
    else if (typeof foo === "number") {
        // 这里 foo 被收窄为 number 类型 
    } 
    else { 
        // foo 在这里是 never 
        const check: never = foo;
    } 
}

image.png

这段代码在现在的 Foo 类型下可以正常运行,但是假如在之后的某一天,Foo 的类型被修改为

type Foo = string | number | boolean;

并且忘记修改函数中的控制流程,那么最后会将 boolean 类型赋值给 never 类型,这时就会产生一个编译错误。

image.png

通过这个方式,我们可以确保 controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。

通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

总结

本文简要的介绍了 Typescript 的类型概念,并且演示了 交叉类型联合类型nerver 几种特殊的 Typescript 类型,通过这篇文章,应该能对 Typescript 类型有一个更加深刻的认识,而不是简简单单的去学会怎么使用。

引用

1.2W字 | 了不起的 TypeScript 入门教程 - 掘金 (juejin.cn)

(19条消息) 读懂 TS 中联合类型和交叉类型的含义_fe_lucifer的博客-CSDN博客

通俗易懂的 TYPESCRIPT 入门教程 (baidu.com)

TypeScript中的never类型具体有什么用? - 知乎 (zhihu.com)

高级类型 · TypeScript中文网 · TypeScript——JavaScript的超集 (tslang.cn)