最近公司组织技术分享,作为一个懒癌患者,终于可以更新一下了前言
- 「类型体操」一词,最早出现在 2006 年
Haskell
的文档。 - 本文需要读者有一定的
Typescript
使用经验、熟悉Typescript
官方文档。 - 本文旨在帮助开发者深入理解
Typescript
类型系统,并灵活运用「类型体操」实现各种各样类型变换的需求。文中将类型类比做集合,帮助初学者更好地理解「类型体操」底层逻辑。但类型本质上并不等价于集合,在某些细节上必然存在出入,需要开发者根据实际情况自行甄别。如发现内容有误,欢迎指正。
- 本文定位是「手册」,贯彻「即插即用」「看的到抄的走」的理念,故不会涉及太多理论性内容的讲解。
- 目前各大知识分享社区关于「类型体操」已经很多优秀的文章,如:Typescript 类型体操天花板,用类型运算写一个 Lisp 解释器、使用Typescript类型体操实现一个简易版扫雷游戏 等。但这些文章存在一定的阅读门槛,也很难「抄的走」,故本文期望能降低门槛,补齐知识盲区,梳理知识结构,让更多人有志之士能掌握这一技能。
- 类型体操虽好,可不能贪杯啊!(具体业务场景中是否适用,还请自行斟酌)
- 本文是【万字长文】深入理解 Typescript 高级用法 内容补充,如发现部分内容理解困难,可与上述文章一起食用。
"Hello, world"
模板字符串类型模板字符串类型 最早从 TS 4.1
版本开始出现,并在后续版本中不断增强。 一般为如下形式:type World = "world";type Greeting = `hello ${World}`;
number一般指数字类型的总称,包含 int
、float
boolean一般指 true
、false
的总称复合类型通用 K/V 结构一般形如下述结构:// interfaceinterface MyKVStructure {[key in string | number | symbol]: any;}// type aliastype MyKVStructure = {[key in string | number | symbol]: any;}
显然 通用 K/V 结构 包含两个可供设置类型的「槽位」[^5],其中「键」类型只能为string
、number
、symbol
,而「值」类型理论上可以为任何类型。补充阅读:Differences Between Type Aliases and InterfacesArray、Tuple
Array
本质上只是特殊形式的 K/V 结构,常用声明方式为Array
、或string[]
;Tuple
是特殊形式的数组,详见typescript handbook#Tuple Types
// 这里的 T 就是类型参数interface MyType {a: T}
在某些简单的场景下,我们也可以把它作为「类型模板」来用函数类型常见的形如 => R
的结构 函数有如何几种常见声明方式:type
MyFunction
=
=>
void
;
interface
MyFunction
{
:
void
;
}
type
MyFunction
=
{
:
void
;
}
各类型常见场景基础类型常见场景string
、number
、boolean
这类基础类型常用于「声明变量」Array、TupleArray
常见于数组相关类型的描述Tuple
作为Array
类型的「子集」在日常开发场景中并不多见,通常大家都会选择使用Array
类型来实现描述数组类型的描述Tuple
有一个专用场景:用于描述「函数参数」类型,故TS 4.0
版本以后新增了Labeled Tuple
功能支持 详见官方文档Array
提供了一个可供设置类型的槽位「槽位」[^5],如果设置多个,只能使用Union Type
,而Tuple
提供了多个可供设置类型的「槽位」[^5],可以清晰、有序、准确地描述一组/多组类型,故在进行多个「类型推导」时,常使用Tuple
存储 输入值/中转值。
Function
类型常用于函数类型描述Function
类型有两个可以 设置类型 的「槽位」,分别为:「参数类型」、「返回值类型」。Function
类型的「参数类型」是「双向协变」的,而「返回值类型」是「协变」的,可以利用此特性实现类型逻辑的「反转」与「映射」。
K/V
结构,而非直接用 object
来代指。 实际上 Typescript
遵循的是「结构化类型」规范,也就是人们常说的「鸭子类型」。 换言之,针对 K/V
结构类型,只要满足「给定结构兼容」,那么就可以判定二者兼容。详见 typescript handbook structural-type-systemSubtype
与 Assignment
在官网的解释中,Typescript
存在两种「类型兼容」方式:Subtype
、Assignment
。
... In Typescript, there are two kinds of compatibility:subtype
andassignment
...
Assignment
详细规则见下表: Ts与集合名词类比对照表名词 | 集合 | Typescript |
---|---|---|
包含于 | ⊆ | extends |
交集 | ∩ | & |
并集 | ∪ | | |
空集 | ∅ | never |
全集 | U | unknown |
⊆
B使用 extends
关键字 例:type TestUnknown = T extends unknown 'Y' : 'N';type TestNever = T extends never 'Y' : 'N';// 任何集合都包含于全集TestUnknown; // Y// 任何集合都不包含于空集TestNever; // N
A
∩
B使用 &
符号A ∩
∅ = ∅例:// 任何集合与空集的交集都是空集type Test = string & never; // never
A ∩
U = A例:// 任何集合与全集的交集都是自己
type
Test
=
string
&
unknown
;
// string
常见运用:- 利用
unknown
剔除「交叉类型」中某些不需要的类型 - 利用 never 实现「一着不慎,满盘皆输」的判断操作(例如 js 中 Array.prototype.some 的表现)
- 与
|
联合使用实现批量过滤
∪
B使用 |
符号A ∪
U = U例:// 任何集合与全集的并集都是全集type Test = { b: number } | unknown; // unknown
A ∪
∅ = A例:// 任何集合与空集的并集都是自己type T1 = string | never; // { b: number }
常见运用: - 剔除「联合类型」中某些不需要的类型,常见使用范例如:某些「内置范型操作符」:
Exclude
、Extract
、Omit
等 - 利用
unknown
实现「一着不慎,满盘皆输」的判断操作(例如js
中Array.prototype.some
的表现) - 与
&
联合使用实现批量过滤
if ... else
或者「三元运算」来实现「条件运算」。「三元运算」在常见编程语言中有一个固定的格式:条件表达式 表达式1 : 表达式2
在 Typescript
类型系统中,我们同样使用类似「三元运算」的方式来实现「条件运算」,这被称为 Conditional Types「条件表达式」本质上最终会返回 boolean
类型值,在 Typescript
类型系统中,依然遵循此规则。在 typescript
中常常使用 例:type MyType = true extends boolean 'Y' : 'N'; // Y
循环/遍历在常见的编程语言中,我们常常需要对 Array
、Tuple
结构进行 循环/遍历,语言本身也内置了很多方法帮助我们完成这样的需求。但是在 Typescript
类型系统 中,并不支持这样直接地 循环/遍历 像 Array
、Tuple
这样的结构。Union Type
的 古怪
表现type MapType
我们发现 Union Type
在经过某些 泛型操作符
后 "裂开了" Union Type
更像是一个「非空有序集合」下述为 type T = string | number
的 AST
描述:再来看一下 getIn
的入参校验:解题思路:get
方法比较简单,直接使用 function
的反向类型推导,根据入参 K
,取出返回值 T[K]
的类型
getIn
方法比 get
方法稍复杂一些,我们需要一个包含所有 key 的组合 联合类型
。例如:interface Obj { a: string; b: {c: number }}
上述这样一个结构,我们希望生成:["a"] | ["b", "c"]
这样的一个 联合类型
。要完成上述需求,我们需要:
- 递归地暴力枚举所有的 key 和 value
- 判断 value 如果是非嵌套类型,那么把对应的 key push 到上一次的 tuple 里,如果依然是嵌套类型,那么继续递归下去
push
功能来实现最终结果的收集。getByKeyPathStr
方法就很简单了,我们只需要基于 getIn
方法,使用类似 join
的操作,把入参的 ["a"] | ["b", "c"]
转换为 "a" | "b.c"
,这里为了简化代码,还是用到了 todash 中的 join
方法。总结:- 这里用到了文章最初讲到的 递归、暴力枚举类型。
- 这里利用了联合类型的 非空、有序、唯裂开的特性。
- 上述代码为了利于讲解,去除了js逻辑部分,有兴趣的同学可以补齐。
- 上述代码中
getIn
、getByKeyPathStr
方法为了简化场景,只完成了入参部分的类型推导,有兴趣的同学可以补齐返回值部分。 - 入参类型推导其实还有缺陷,实际应为
["a"] | ["b"] | ["b", "c"]
,但改动其实不大,有兴趣的同学可以修正一下。
declare
声明的类型,有时在全局生效,有些只能在局部生效?- 首先确保你自定义的类型均已被 ts 加载
- 检查你所编写的内容属于「脚本」还是属于「模块」,常见区分方法为:使用了
import
、export
关键字则为「模块」。详见「typescript handbook」#modules - 如果是「脚本」,直接
declare
即可在全局生效,若为「模块」,则仅在局部生效
declare global {interface String { // ...}}
详见「typescript handbook」#global modifying module【接上】「脚本」内如何引入其他类型?// 如果是 类库/// // 如果是 自定义文件///
详见「typescript handbook」#reference types如何扩展一个库/模块内部的类型?详见官方文档我是一个伸手党,我想拿来就用,除了官方内置的泛型操作符有没有其他现成好用的库?- ts-toolbelt
- todash
- 请阅读文档
- 请仔细阅读文档
- 请熟读并背诵文档
名词解释
- 非空有序集合:非空:指每一项元素不为空;有序:指该列表保持有序;集合:指每一项元素唯一。