TypeScript的本质: 类型编程

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

类型别名

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

答案是引用和复用

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

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

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

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

js 复制代码
type Handler = () => void;

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

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

js 复制代码
type User =  {
  userName: string;
  userAge: number;
  userMarried: boolean;
  userJob?: string;
}

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

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

联合类型

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

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

js 复制代码
type PossibleTypes = string | number | boolean;

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

js 复制代码
let foo: PossibleTypes = 'linbudu';

foo = 599;
foo = true;

字面量类型

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

js 复制代码
type Status = 'success' | 'failure';
type Code = 200 | 404 | 502;

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

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

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

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

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

js 复制代码
const fixedStr: 'linbudu' = 'linbudu'; // 值只能是 'linbudu'
const fixedNum: 599 = 599; // 值只能是 599

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

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

js 复制代码
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];

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

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

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

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

js 复制代码
interface VisitorUser {}
interface CommonUser {}
interface VIPUser {}
interface AdminUser {}

type User = VisitorUser | CommonUser | VIPUser | AdminUser;

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

交叉类型

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

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

js 复制代码
interface UserBasicInfo {}
interface UserJobInfo {}
interface UserFamilyInfo {}

type UserInfo = UserBasicInfo & UserJobInfo & UserFamilyInfo;

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

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

类型编程

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

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

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

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

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

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

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

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

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

js 复制代码
type Status<T> = 'success' | 'failure' | 'pending' | T;

type CompleteStatus = Status<'offline'>;

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

js 复制代码
function Status(T){
  return ['success', 'failure', 'pending', T]
}

const CompleteStatus = Status('offline');

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

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

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

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

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

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

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

js 复制代码
function factory<T>(input: T): T {
  // ...
}

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

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

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

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

相关推荐
MiyueFE18 小时前
🚀🚀五个前端开发者都应该了解的TS技巧
前端·typescript
ttod_qzstudio20 小时前
基于typescript严格模式以实现undo和redo功能为目标的命令模式代码参考
typescript·命令模式
张志鹏PHP全栈20 小时前
TypeScript 第十天,TypeScript面向对象之Class(二)
前端·typescript
慧一居士21 小时前
ESLint 完整功能介绍和完整使用示例演示
前端·javascript·typescript
enzeberg2 天前
TypeScript 工具类型(Utility Types)
typescript
難釋懷2 天前
TypeScript类
前端·typescript
杰哥焯逊2 天前
基于TS封装的高德地图JS APi2.0实用工具(包含插件类型,基础类型)...持续更新
前端·javascript·typescript
工业甲酰苯胺3 天前
TypeScript枚举类型应用:前后端状态码映射的最简方案
javascript·typescript·状态模式
土豆骑士4 天前
简单理解Typescript 装饰器
前端·typescript
ttod_qzstudio4 天前
彻底移除 HTML 元素:element.remove() 的本质与最佳实践
前端·javascript·typescript·html