平常我们编写 TypeScript 时,主要会使用类型注解(给变量、函数等加上类型约束),这可以增强代码可读性、避免低级 bug。实际上 TypeScript 的类型系统设计的非常强大,强大到可以单独作为一门编程语言。本文是自己学习 TypeScript 类型编程的一个总结,希望对你有帮助。
开始之前
本文不会对 TypeScript 的基础语法和使用进行说明,你可以参考互联网上提供的优秀资料:
启程
参考 SCIP 中对于编程语言的描述。一门编程语言应该提供以下机制:
- 基本表达式。用来表示语言所关心的最简单的个体。
- 组合的方法。从简单的个体出发构造复合的对象。
- 抽象的方法。能将复合对象封装作为独立单元去使用。
下面我们将以这三个方面为线索来探索 TypeScript 的类型编程。
基本表达式
我们首先来看看类型编程中,定义“变量”的方式:
1 | // string、number、boolean 的值可以作为类型使用,称为 literal type |
这里稍微补充下 interface 和 type 的区别。
最主要的区别就是 type 可以进行“类型编程”,interface 不行。
interface 能定义的类型比较局限,就是 object/function/class/indexable:
1 | // object |
interface 可以被重新“打开”,同名 interface 会自动聚合,非常适合做 polyfill。比如,我们想要在 window 上扩展一些原本不存在的属性:
1 | interface Window { |
组合的方法
有了基本表达式,我们来看组合的方法。
| 和 & 操作符
& 表示必须同时满足多个契约,| 表示满足任意一个契约即可。
1 | type Size = 'large' | 'normal' | 'small'; |
keyof 操作符
1 | interface Sizes { |
抽象的方法
抽象的方法实际上指的就是“函数”。我们来看看类型编程中,“函数”该怎么定义。
1 | // “定义” |
看起来是不是和编程语言里面的函数很相似?这些“函数”的输入(参数)是类型,经过“运算”后,输出是“类型”。接着我们来看看在“函数体”(也就是等号右边)里面除了一些基本操作外,还可以做些其他什么骚操作。
“映射”操作(mapped)
将已有类型转换为一个新的类型,类似 map。返回的新类型一般是对象。
1 | type MakeRecord<T extends keyof any, V> = { |
条件——extends
条件类型可以理解为“三元运算”,T extends U ? X : Y,extends 可以类比为“相等”。
1 | // 只保留string |
“析构“——infer
infer 可以理解为一种“放大镜”机制,可以“捕获”到被“嵌”在各种复杂结构里的类型信息。
1 | // 对象 infer,可以取得对象某个属性值的类型 |
可以发现上面的例子需要使用 infer,是因为我们在“定义”时不知道具体的类型,需要在“调用”时做“推断”。infer 帮我们标注了待推断的类型,最终计算出实际的类型。
嵌套&递归
在“函数体”中,我们其实可以再“调用函数“,形成一种嵌套和递归的机制。
1 | // 取函数第一个参数的类型 |
尾声
文章写到这里基本就结束了,这篇文章的内容可能在平常的开发中会比较少遇到,但是对于补全自己的 TypeScript 体系、开阔视野还是有所帮助的。如果想更多的来些实战演练,推荐看看这个:https://github.com/type-challenges/type-challenges