「TS类型体操」🔥巧用TS特殊类型特性(上)

657 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

TypeScript中,除了有一些和js重叠的类型,如numberstringboolean等,但也有一些是ts特有的,比如anynever联合类型,这些特殊类型有它们自己的特性,当我们需要判断一个类型参数是否是这些特殊类型时,就可以根据它们各自的特性去实现判断逻辑

除此之外,在TypeScript的世界里,类class也相较js做了扩展,属性可以用privateprotectedpublic等修饰符来描述成员属性的可见性,同样地,它们也有自己的特性,我们可以巧用这些特性去实现工具类型,判断一个属性是否是类的私有属性等功能

接下来我就会巧用这些特性,实现上述的这些工具类型

IsAny -- 判断类型是否是any

假设现在有下面这个需求,让你实现这样一个工具类型,接收一个泛型参数,判断传入的泛型是不是any类型

IsAny<any> // ==> true
IsAny<number> // ==> false
IsAny<string> // ==> false

这就需要利用到any的特性了

  1. ts中,any和任何类型做交叉运算,得到的仍然是any
type A = any & 1 // any
type B = any & number // any
type C = any & string // any
type D = any & boolean // any
// ...
  1. 任何类型都是any的子类型,可以通过extends进行类型约束验证这一点
type A = number extends any ? true : false // true
type B = string extends any ? true : false // true
type C = boolean extends any ? true : false // true
type D = 1 extends any ? true : false // true
type E = 'plasticine' extends any ? true : false // true

于是我们就可以利用这两个特性,去实现IsAny,怎么实现呢?

根据特性2,我们可以用条件类型判断,条件为判断任意一个类型是否extends泛型参数和任意类型交叉运算的结果,如果是的话,说明泛型参数是any,因为只有any与类型交叉运算后得到的才会是any,然后判断任意类型 extends anytrue就说明泛型参数是any

这里的任意类型我们可以随便选一个,只要不是any就行,主要是起到一个占位的作用

type IsAny<T> = number extends T & number ? true : false

IsEqual -- 判断两个类型是否相等(包括对any的判断)

如何判断两个类型是否是同一个类型呢?一个简单的实现可能是下面这样:

type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false)

type Res = IsEqual<string, number> // false
type Res1 = IsEqual<'1', '2'> // false
type Res2 = IsEqual<true, true> // true
type Res3 = IsEqual<'a', any> // true

可以看到,前三个都没啥问题,但是最后一个IsEqual<'a', any>的结果是false,可实际上类型'a'不应当和any是同一个类型,它们只有相关性,也就是'a'属于any,而any也可以代表'a'类型,所以这样的写法是无法判断出any类型的相等性的,我们目前要判断的是两个类型是否相等,而不是相关

这就先要聊聊ts源码级层面的内容了,在ts中,先看看两个条件类型之间的判断:

(T1 extends U1 ? X1 : Y1) extends (T2 extends U2 ? X2: Y2) ? true : false

对于两个条件类型的判断结果,在ts源码中的判断逻辑是

  • T1T2相关
  • X1X2相关
  • Y1Y2相关
  • U1U2相等

类型相关就比如1 extends numbertrue,但是1number并不相等

可以看到,虽然ts没有提供判断两个类型相等的途径,但是源码层面上是有这样一个地方可以判断两个类型严格相等的,而我们恰好就可以利用这个特性去判断两个类型是否相等,只要把待判断的两个类型放到U1U2的位置即可

/**
 * @description 判断两个类型是否相等 -- 是相等不是相关
 *
 * 相等和相关的区别 -- 1 和 any 相关 因为 1 extends any 是 true
 * 但是如果要说 1 和 any 是同一个类型吗?那显然是不对的
 * 所以我们的 IsEqual<1, any> 应为 false 而不是 true
 */

// 无法判断 any 类型
// type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false)

// 利用 extends 判断两个条件类型时会有严格相等判断的逻辑实现类型相等判断
// (T1 extends U1 ? X1 : Y1) extends (T2 extends U2 ? X2 : Y2) ? true : false
// T1 和 T2 相关
// X1 和 X2 相关
// Y1 和 Y2 相关
// U1 和 U2 相等
// 满足这四个条件时才会是 true
type IsEqual<A, B> = (<T>() => T extends A ? true : false) extends <
  T,
>() => T extends B ? true : false
  ? true
  : false

type Res = IsEqual<string, number> // false
type Res1 = IsEqual<'1', '2'> // false
type Res2 = IsEqual<true, true> // true
type Res3 = IsEqual<'a', any> // true

IsUnion -- 判断类型是否是联合类型

首先要了解联合类型的特性,当联合类型作为extends关键字的左边部分时,联合类型是会被拆分成单独元素传入的,所以可以利用这点来判断一个类型是否是联合类型

// 利用联合类型作为 extends 的左边部分时会被拆开传入的特性来判断 A 是否是联合类型
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never

type Res = IsUnion<'a' | 'b'> // true
type Res1 = IsUnion<'a'> // false

IsNever -- 判断类型是否是never

never的特性:当作为条件类型extends左边的部分时,条件类型的结果直接就是never

type IsNever<T> = T extends never ? true : false
type Res = IsNever<never> // never

但是现在我们要的不是never,而是truefalse,这时候就可以改变一下,把类型参数用数组包起来,变成一个元组类型,从而得到布尔值的结果

type IsNever<T> = [T] extends [never] ? true : false

type Res = IsNever<never> // true
type Res1 = IsNever<1> // false

IsTuple -- 判断类型是否是元组类型

一个简单的想法可能是下面这样,直接判断元素是否是一个数组

type IsTuple<T> = T extends [...els: unknown[]] ? true : false

type Res = IsTuple<number[]> // true
type Res1 = IsTuple<[1, 2, 3]> // true

看上去好像没毛病,但实际上,number[]是数组类型,而[1, 2, 3]则是元组类型,二者有何区别呢?

  • 数组类型长度不固定,通过['length']索引访问其长度时,得到的是number类型
  • 元组类型长度固定,通过['length']索引访问其长度时,得到的是具体的数字,比如12
  • 元组类型的元素都是readonly的,也就意味着不能够修改元组中的元素

可以在上面那个实现的基础上,添加对readonly的判断以及对length属性是否为number的判断,即可实现对元组类型的判断

// 判断两个类型不相等
type NotEqual<A, B> = (<T>() => T extends A ? true : false) extends <
  T,
>() => T extends B ? true : false
  ? false
  : true

type IsTuple<T> = T extends [...els: infer Els]
  ? NotEqual<Els['length'], number>
  : false

type Res = IsTuple<number[]> // false
type Res1 = IsTuple<[1, 2, 3]> // true

NotEqual的实现就是在IsEqual的基础上,对调最后的布尔值位置即可实现取反的效果