TS 类型体操 之 extends,Equal,Alike 使用场景和实现对比
在程序中,判断相等是一个很重要的内容,在 TS 中判断相等(其实是属于 xxx 范围) 用的是 extends
。只用 extends 自然是不够,所以有很多工具类有一些很微妙的技巧来实现严格相等的功能
下面会讲到几个工具类
Equal
和Alike
。这 2 个并不是 TS 的官方实现,而且在 TS 体操练习的仓库里面大神封装的工具类
extends 的作用
先回顾一下 extends 的作用和多数的应用场景
// 🌰 - 1
type ID = string | number
type TestID = ID extends string ? true : false // false
type TestID2 = string extends ID ? true : false // true
// 🌰 - 2
type UnionType = string | number | boolean
type testUnion = string extends UnionType ? true : false // true
type testUnion2 = [string] extends [UnionType] ? true : false // true
type testUnion3 = [UnionType] extends [string] ? true : false // false
上面的 2 个例子中
ID 是 string/number 类型,所以 ID 取值的范围是比 string 类型的范围的大的,TestID 返回值自然为 false 了
而一个 string 类型,是 在 string|number 范围内的,所以自然为 true 了
其他在数组中的也是同理
用 extends 的特性解决 01097-medium-isunion
extends 是一个自带范围判断的判断符,只要在另外一个类型范围内,那就判定为 true
01097-medium-isunion 题目的需求:判断一个传入的类型是否 union 类型
什么是 union(联合)类型呢,就是由多个类型 联合而成,其中
|
就是为了 粘合多个类型的
测试用例如下:非常有代表性
type cases = [
Expect<Equal<IsUnion<string>, false >>,
Expect<Equal<IsUnion<string|number>, true >>,
Expect<Equal<IsUnion<'a'|'b'|'c'|'d'>, true >>,
Expect<Equal<IsUnion<undefined|null|void|''>, true >>,
Expect<Equal<IsUnion<{ a: string }|{ a: number }>, true >>,
Expect<Equal<IsUnion<{ a: string|number }>, false >>,
Expect<Equal<IsUnion<[string|number]>, false >>,
// Cases where T resolves to a non-union type.
Expect<Equal<IsUnion<string|never>, false >>,
Expect<Equal<IsUnion<string|unknown>, false >>,
Expect<Equal<IsUnion<string|any>, false >>,
Expect<Equal<IsUnion<string|'a'>, false >>,
]
根据上面 extends 的介绍,我们就可以利用下面的原理解决这个问题
string extends string | number
为 truestring | number extends string
为 false- TS 体操中的 Union(联合类型)会自动 “解构”
答案如下:
type IsUnion<T,U = T> = T extends U ? U extends T ? false : true : false
在 IsUnion
里面,T 会自动的 “解构”,就比如 传入的是 string|number
。
- T 会自动变成 string,然后和 U 进行比较
- T 在变成 number ,在和 U 比较
- U 同理 变成 string 和 T(string/number) 进行比较
所以看上去 T extends U
只是一个三目运算符,实际上他们已经运行 4 次比较了,只要有一次为 false,那 extends 的结果就为 false
不信?插个题外话证明下
我们把类型换成 'a' | 'b'
, 这个也是个 union 类型,然后我们把 extends 换成他们 2 个拼接
type TestUnion<T extends string, U extends string = T> = `${T}-${U}`
type ResultUnio = TestUnion<'a' | 'b'>
// type ResultUnio = "a-a" | "a-b" | "b-a" | "b-b"
看到结果后,应该就能理解上面说的,union 类型会自动的 “解构” 的意思了把,T 会自动解构为 a 和 b,U 也会自动解构为 a 和 b,然后两两配对组合;最终得出结果(不明白的在琢磨琢磨)
能理解这个 demo 后,后面还有几道题会用到这个知识点,圈起来要考
题外话结束
说回测试用例的几个例子
-
{ a: string|number }
和[string|number]
不是联合类型,因为他们其实都属于同一个对象下的,他们的属性值是联合类型,而他们自身并不是 -
剩下的最后 4 个测试用例中,比如
string|never
,string|'a'
也是不符合 多个类型联合 的意义- never 说明没有一个类型符合,就好像绝育的小猫咪和正常的小猫咪还能生出新的小猫咪吗?不行的嘛。所以他们不能联合
- 至于
string|'a'
‘a’ 本来就是属于 string 类型的,这就好像 一只公的小猫和一只公的黑色小猫,他们能 联合 吗?他们都属于公猫
,只有并集,联合不出来结果 - 所以
string|any
同上面同理,any 包罗一切,没有东西可以和 any 组成联合
但是!他们虽然不能组成联合类型,但是这不会报错,像这种 脱裤子放屁的操作
TS 会自动帮他们取范围比较大的一个类型作为联合结果
就好比 一只公的小猫和一只公的黑色小猫,他们统称为 公猫
type StringUnion = string | 'a' // 结果为 string
type StringUnion2 = string | any // 结果为 any
使用小技巧实现 Equal 全等判断
要说 Equal 的使用场景,TS 类型体操的练习题每一题都用到了
type User = {
name?: string
age: number
address: string
}
type User2 = {
name?: string
} & {
age: number
address: string
}
type R = Equal<User1, User2> // false
type R2 = User1 extends User2 ? (User2 extends User1 ? true : false) : false // true
看上面的 demo,如果仅用 extends
来判断,他们是相等的(U1 == U2 && U2 == U1)
可是写法上确实有差别,User2 是通过交叉类型交叉在一起的
这也就是为什么 extends 做不了完全相等的原因
所以就出现了 Equal
的方案,看看 Equal
的实现
export type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
一开始我也琢磨了很久
- T 是那里冒出来的?
- T 是作为泛型,传入一个函数内,然后在返回出来?
- T 为什么会等于 X ?
- T 又为什么等于 Y 了 ?
最后在这里看到了答案,略有感悟 How does the Equals work in typescript?
根据有括号先看括号的原则 把 Equal 的等式分成 3 段内容来看
(<T>() => T extends X ? 1 : 2)
假设为 1 式(<T>() => T extends Y ? 1 : 2)
假设为 2 式
连起来看就是 : 1式
extends 2式
? true : false
而 <T>()
怎么理解呢?有一句很重要的话 (也是摘自 stackoverflow 上的答案)
The assignability rule for conditional types <…> requires that the types after extends be “identical” as that is defined by the checkerF
对应翻译: 条件类型 <…> 的可分配性规则要求扩展后的类型与检查器定义的类型“相同”
根据上面说的 1式
,2式
来个 demo 理解下:
declare let x: <T>() => T extends number ? 1 : 2
declare let y: <T>() => T extends number ? 1 : 2
declare let z: <T>() => T extends string ? 1 : 2
var str: string = '1'
str = 2 // 报错
x = 100 // 报错
x = y
x = z // 报错
y = z // 报错
- 2 赋值给 str 时会报错 Type ‘number’ is not assignable to type ‘string’.
- x 赋值为 100 时会报错 Type ‘number’ is not assignable to type ‘<T>() => T extends number ? 1 : 2’.
仔细观察这 2 个错误消息,他们都属于
ts(2322)
号错误。那么就可以理解为<T>() => T extends number ? 1 : 2
实际上和string
一样,是一个单一类型,而不是我们认为的函数
-
y 赋值给 x 啥事都没有
-
z 赋值给 x 和 z 赋值给 y 都会提示下面的错误
Type '<T>() => T extends string ? 1 : 2' is not assignable to type '\<T\>() => T extends number ? 1 : 2'.
Type 'T extends string ? 1 : 2' is not assignable to type 'T extends number ? 1 : 2'.
Type '1 | 2' is not assignable to type 'T extends number ? 1 : 2'.
Type '1' is not assignable to type 'T extends number ? 1 : 2'.
说白了就是不同类型的不能赋值,因为 可分配性规则要求扩展后的类型与检查器定义的类型“相同”
看到这里,是不是大概就理解了?1 式 和 2 式其实最后只是一个写法比较夸张的 类型。而后面接的小尾巴(1 和 2,也真的是为了凑字数,为了 extends 凑齐字数用的)
比如稍微改一下,等式就全都报错了
而改一下泛型的定义,倒不会报错
总结起来还是那句话
条件类型 <…> 的可分配性规则要求扩展后的类型与检查器定义的类型“相同”
看懂了 demo 和解析的文字,那么看懂 Equal 也不在话下了,只要 1 式和 2 式 extends,在基于上面的“相同”特性,那就是全等了
和 Equal 很像的 Alike
Alike 和 Equal 一样是为了判断相等的,在之前一篇文章 《TS 类型体操 之 循环中的键值判断,as 关键字使用》 中有讲过 的测试用例就是用的 Alike
type User = {
name?: string
age: number
address: string
}
type User2 = {
name?: string
} & {
age: number
address: string
}
// 把 Equal 换成 Alike 得到的就是 true 的结果
type R = Alike<User1, User2> // true
type R2 = User1 extends User2 ? (User2 extends User1 ? true : false) : false // true
如果用 Alike,那么刚才 Equal 为 false 的 2 个对象又能重新变为 true 了
Alike 不能就单纯的理解是就是 T extends U && U extends 的实现,绝对不能
- Alike 实现如下:
export type MergeInsertions<T> = T extends object ? { [K in keyof T]: MergeInsertions<T[K]> } : T
export type Alike<X, Y> = Equal<MergeInsertions<X>, MergeInsertions<Y>>
使用了 MergeInsertions
的工具类,这个的作用和昨天讲的 type Clone<T> = Pick<T, keyof T>
差不多!只是说我的 Pick 只能 Pick 一层,而 MergeInsertions
则是考虑到了对象多层嵌套的情况(升级版 Clone,有点浅拷贝和深拷贝的意思)
{} & {}
经过 MergeInsertions 处理后,也会被合并为一个对象 {}
,然后在拿去 Equal 对比。不得不说,真是妙
上面提到 Alike 不能就单纯的理解是就是 T extends U && U extends 的实现
看个案例:
type TestUnion = { a: string } | { a: number }
type IsLike = Alike<TestUnion, TestUnion> // true
type IsUnionResult = IsUnion<TestUnion> // true
type IsLike2 = Alike<string, string> // true
type IsUnionResult2 = IsUnion<string, string> // false
对于单一类型,T extends U && U extends 可以判断出是否 Union ,而 Alike 只能判断他们是否相等
所以要想判断 IsUnion 还得靠双 extends,Alike 只是一个宽松的相等运算符
总结
- extends 是一个 子集的比较,只要 x 是 y 的子集,那么 x extends y 就为 true
- 判断一个类型是否 union 类型,用到的也是 extends 子集 的特点
- 如果 x 是 y 的子集,那么 y 就不可能是 x 的子集
- x extend y 并且 y 又 extends x 的话,那只能说他们都是 单一的类型,不存在联合
- Equal 函数很巧妙的用了 TS 的一个特性
The assignability rule for conditional types <...> requires that the types after extends be "identical" as that is defined by the checkerF
- 通过一个类似公式代入的场景判断 2 个类型是否全等
- 单一类型 和 交叉类型
不全等
- 如果想把条件放宽松点,只想判断 2 个类型所有的字段都相同就是相等的话
- 就要用上 Alike ,Alike 则是用了一个
MergeInsertions
(Clone 升级版) 来把交叉类型合并为 单一类型 - 有了单一类型,就可以用 Equal 来对比了
- 对于单一类型,T extends U && U extends 可以判断出是否 Union ,而 Alike 只能判断他们是否相等(所以不要搞混 Alike 和 IsUnion 的实现原理)
- 就要用上 Alike ,Alike 则是用了一个
无论是 IsUnion
的实现原理,还是 Alike
的宽松语法对比,Equal
的严格全等,extends
的范围子集判断;都有各自的使用场景,日常做题/开发都要 看题下方案