• TS 类型体操 之 extends,Equal,Alike 使用场景和实现对比


    TS 类型体操 之 extends,Equal,Alike 使用场景和实现对比

    在程序中,判断相等是一个很重要的内容,在 TS 中判断相等(其实是属于 xxx 范围) 用的是 extends。只用 extends 自然是不够,所以有很多工具类有一些很微妙的技巧来实现严格相等的功能

    下面会讲到几个工具类 EqualAlike。这 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上面的 2 个例子中

    ID 是 string/number 类型,所以 ID 取值的范围是比 string 类型的范围的大的,TestID 返回值自然为 false 了

    而一个 string 类型,是 在 string|number 范围内的,所以自然为 true 了

    其他在数组中的也是同理

    理解上面举的例子的原理,就可以解决了 1097・IsUnion 这道题

    用 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 >>,
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    根据上面 extends 的介绍,我们就可以利用下面的原理解决这个问题

    • string extends string | number 为 true
    • string | number extends string 为 false
    • TS 体操中的 Union(联合类型)会自动 “解构”

    答案如下:

    type IsUnion<T,U = T> = T extends U ? U extends T ? false : true : false
    
    • 1

    IsUnion 里面,T 会自动的 “解构”,就比如 传入的是 string|number

    1. T 会自动变成 string,然后和 U 进行比较
    2. T 在变成 number ,在和 U 比较
    3. 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"
    
    • 1
    • 2
    • 3
    • 4

    看到结果后,应该就能理解上面说的,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
    
    • 1
    • 2

    使用小技巧实现 Equal 全等判断

    要说 Equal 的使用场景,TS 类型体操的练习题每一题都用到了

    我是在 2757・PartialByKeys 这一题才特别注意到的

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    看上面的 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
    
    • 1
    • 2
    • 3

    一开始我也琢磨了很久

    • 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 // 报错
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 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
    • 3
    • 4

    说白了就是不同类型的不能赋值,因为 可分配性规则要求扩展后的类型与检查器定义的类型“相同”

    看到这里,是不是大概就理解了?1 式 和 2 式其实最后只是一个写法比较夸张的 类型。而后面接的小尾巴(1 和 2,也真的是为了凑字数,为了 extends 凑齐字数用的)

    比如稍微改一下,等式就全都报错了

    而改一下泛型的定义,倒不会报错

    总结起来还是那句话

    条件类型 <…> 的可分配性规则要求扩展后的类型与检查器定义的类型“相同”

    看懂了 demo 和解析的文字,那么看懂 Equal 也不在话下了,只要 1 式和 2 式 extends,在基于上面的“相同”特性,那就是全等了

    和 Equal 很像的 Alike

    Alike 和 Equal 一样是为了判断相等的,在之前一篇文章 《TS 类型体操 之 循环中的键值判断,as 关键字使用》 中有讲过 8・Readonly 2的测试用例就是用的 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果用 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>>
    
    • 1
    • 2
    • 3

    使用了 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对于单一类型,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 的实现原理)

    无论是 IsUnion 的实现原理,还是 Alike 的宽松语法对比,Equal 的严格全等,extends 的范围子集判断;都有各自的使用场景,日常做题/开发都要 看题下方案

  • 相关阅读:
    MAC地址表泛洪攻击
    Unity ECS实例:制作俯视角射击游戏!
    Arduino与Proteus仿真-Nokia5110 LCD界面菜单仿真
    SSM整合thymeleaf
    PostgreSQL逻辑复制解密
    BZOJ4756 Promotion Counting(线段树合并)
    【网络】抓包工具Wireshark下载安装和基本使用教程
    SpringBoot自动配置(装配)流程
    【Educoder作业】C&C++结构实训
    ArrayList 源码分析
  • 原文地址:https://blog.csdn.net/Jioho_chen/article/details/125611340