TypeScript的本质: 类型编程

1,146 阅读8分钟

类型编程是学TypeScript中拦路虎,它比较晦涩难懂,今天试着就来从类型别名开始来解剖这只麻雀。

类型别名

在 JavaScript 中,什么时候会需要定义变量和函数?

答案是引用和复用

定义变量,是为了后续对这个值可以直接进行引用,即使有多处使用逻辑,也可以很简单地复用这个变量。

函数也是类似,定义一个函数就是为了抽象一段通用的数据转换逻辑,然后再提供给其它地方的逻辑消费,这样它们就可以用一个函数名来替换掉一大段重复代码了。

在 TypeScript 中,类型别名起到的就是变量的作用,它可以存储一个类型,后续你可以直接引用它即可。

下面,使用类型别名存储一个函数类型:

type Handler = () => void;

const handler1: Handler = () => {};
const handler2: Handler = () => {};

也可以使用类型别名来替换接口,实现对对象类型的复用:

type User =  {
  userName: string;
  userAge: number;
  userMarried: boolean;
  userJob?: string;
}

const user: User = { /* ... */ }

在作为变量的场景中,类型别名还和联合类型、交叉类型有着紧密的结合,接下来就来一探究竟。

联合类型

联合类型它的语法是这样的:A | B | C,是不是很像 JavaScript 中的按位或 A || B || C

需要注意的是,如果你想定义一个联合类型,需要使用类型别名来存放

type PossibleTypes = string | number | boolean;

它所表示的或逻辑,只要你的变量满足其中一个类型成员,就可以被认为满足这个类型,因此你的变量可以在后续被赋值为其它的类型成员:

let foo: PossibleTypes = 'linbudu';

foo = 599;
foo = true;

字面量类型

联合类型对其中的类型成员并没有限制,你可以混合原始类型,字面量类型,函数类型,对象类型等等等等。而在实际应用中,最常见的应该是字面量联合类型,它表示一组精确的字面量类型:

type Status = 'success' | 'failure';
type Code = 200 | 404 | 502;

字面量类型是什么?这到底是类型还是值?

我们知道,一个变量被标记为 string 类型的变量只能被赋值为字符串,换句话说,所有的字符串值都属于 string 类型。

那么这就显得过于宽泛了,如果我们希望将变量类型约束在几个特定的字符串值之间呢?

就比如上面的类型别名 Status,就能表达"这个变量是字符串类型"和"这个变量只能是'success'和'failure'两个字符串"这两个概念。

组成 Status 的这两个"值",其实就是字面量类型,比如你也可以用字面量类型来作为类型标注:

const fixedStr: 'linbudu' = 'linbudu'; // 值只能是 'linbudu'
const fixedNum: 599 = 599; // 值只能是 599

如果你感觉字面量类型和实际值不好区分,其实只要注意它们出现的位置即可,一个同样的字符串,只要出现在类型标注的位置,那指的当然就是类型了

字面量类型是和原始类型以及对象类型对应的——是的,包括对象类型,来看完整的示例:

const literalString: 'linbudu' = 'linbudu';
const literalNumber: 599 = 599;
const literalBoolean: true = true;
const literalObject: { name: 'linbudu' } = { name: 'linbudu' };
const literalArray: [1, 2, 3] = [1, 2, 3];

为什么我们需要字面量类型?

因为字面量联合类型相比它们对应的原始类型,能够提供更精确的类型信息与类型提示,如:

image.png

理想情况下,如请求状态与用户类型这样值被固定在一个小范围内的属性,都应该使用字面量联合类型进行标注,字面量类型和联合类型简直就是天生一对

除了基于字面量类型的小范围精确标注,我们也可以使用由接口组成的联合类型

interface VisitorUser {}
interface CommonUser {}
interface VIPUser {}
interface AdminUser {}

type User = VisitorUser | CommonUser | VIPUser | AdminUser;

const user: User = {
  // ...任意实现一个组成的对象类型
}

交叉类型

搞清楚了联合类型,那交叉类型就很好懂了。

类似于按位或 || 到联合类型的 |,交叉类型的 & 也脱胎自按位与 &&,我们同样可以使用类型别名来表示一个交叉类型。

interface UserBasicInfo {}
interface UserJobInfo {}
interface UserFamilyInfo {}

type UserInfo = UserBasicInfo & UserJobInfo & UserFamilyInfo;

交叉类型的本质,其实就是表示一个同时满足这些子类型成员的类型,所以如果你交叉两个对象类型,可以理解为是一个新的类型内部合并了这两个对象类型:

// 伪代码
type UserInfo = {
  ...UserBasicInfo,
  ...UserJobInfo,
  ...UserFamilyInfo
}

类型编程

现在,我们知道了类型别名、联合类型以及交叉类型的相关知识,也将它们一一类比到了 JavaScript 中的变量、逻辑或与逻辑与,你是否渐渐感觉到了,其实 TypeScript 的本质,是在对类型进行编程

如果能想到这一层,那说明你已经抓住了这门编程语言的本质,即 TypeScript 在 JavaScript 对值进行编程的能力之上,又给予了你对类型进行编程的能力

为什么需要对类型进行编程?

因为,有时候类型世界也存在着和实际值一致的逻辑,就像联合类型与交叉类型,就很好地证明了这一点。

那类型编程的抓手是什么呢?

泛型,它的本质就是类型世界中的参数。这里只是讲下泛型的基本概念和特征,具体的用法后面再讲,泛型是一个难点,当泛型和接口 interface 和 类 class 结合时更加晦涩难懂。

下面以函数为例,来理解泛型的概念。

在绝大部分编程语言中,函数都是一个非常重要的概念,如果缺少了函数,我们的代码可能会变得冗长晦涩,到处夹杂着重复的片段。而在函数中,最重要的概念则是参数,参数是一个函数向外界开放的唯一入口,随着入参的差异,函数可能也会表现出各不相同的行为。

上面提到,类型变量可以充当变量,其实类型别名还能够充当函数的作用,但函数怎么能没有入参?我们可以这么来为类型别名添加一个入参,也就是泛型

type Status<T> = 'success' | 'failure' | 'pending' | T;

type CompleteStatus = Status<'offline'>;

在这个例子中,Status 就像一个函数,它声明了自己有一个参数 T,即泛型,并会将这个参数 T 合并到自己内部的联合类型中。我们可以用一段伪代码来理解:

function Status(T){
  return ['success', 'failure', 'pending', T]
}

const CompleteStatus = Status('offline');

这里的泛型就是参数作用,只不过它接受的是一个类型而不是值。

是不是对类型编程 这个概念又有了一些新的认知。

唯一需要你稍微转换下思维的是,在 TypeScript 中,变量与函数都由类型别名来承担,而一个类型别名一旦声明了泛型,就会化身成为函数,此时严格来说我们应该称它为工具类型

看起来好像类型别名才是主角,泛型的存在感还比较弱,它就是一个默默无闻的参数罢了。

那是因为我们这里展示的是主动赋值的用法,用于帮助你快速建立起对类型编程这个概念的理解。而实际上,自动推导才是泛型的强大之处所在。

我们先回到 JavaScript 中的函数,想象我们有一个这样的函数,它的出参与入参类型是完全一致的,比如给我个字符串,我就返回字符串类型,如果是数字,就返回数字类型,此时你会怎么对这个函数进行精确地类型标注。

这个时候我们就要请出泛型了,前面是把一个类型主动赋值给泛型,而其实人家真正的作用可不仅于此,我们先给这个函数添加上泛型:

function factory<T>(input: T): T {
  // ...
}

可以看到这里我们一共出现了三个 T,它们的作用分别是什么?

首先,类似于类型别名中,<T>声明了一个泛型,而参数类型与返回值类型标注中的 T 就是普通的类型标注了。这里的整体意思其实是:这个函数有一个泛型 T,当你的函数获得一个入参时,会根据这个入参的类型自动来给 T 赋值,然后同时作为入参与返回值的实际类型!

这其中最重要的有两点,自动赋值以及同时作为入参与返回值的实际类型,前者意味着我们无需再操心到底会有哪些可能的类型输入了,后者意味着我们只需要在两处使用同一个泛型参数,就实现了入参与返回值的类型绑定。

前面的知识可能比较好理解,但是泛型可能又让你开始有点迷糊了,毕竟光是类型世界也有参数就需要花上一些功夫来理解。如果你成功绕过来了,那么恭喜你开始慢慢接近类型编程这个光听就很酷炫概念的本质了。