最近在看 TypeScript 相关的内容,做了一下类型体操,真的太秀啦

递归、infer 满天飞,今天就来领略一下 TS 能做什么骚操作吧!

先放上本文的几个小标题,很骚

下面开始军训体操

一、巧用数组上数学课

这一题是 TS 类型应战中的 Greater Than

这道题需求咱们完结GreaterThan<T, U>判别T > Utrue 仍是 false

有几个特别测试用例

GreaterThan<2, 1> //should be true
GreaterThan<1, 1> //should be false
GreaterThan<10, 100> //should be false
GreaterThan<111, 11> //should be true

看到这题,在 JS 中十分的简略,直接就能有答案,可是 TS 是没有核算才能的,也不支持巨细判别

那么咱们还能怎样做呢?巧用数组,经过数组的 length 来进行比较

有两种可行的办法

  • 第一种便是递归,可是经过实践咱们会知道,当参数过大时,很容易爆栈
  • 第二种办法便是结构两个长度为 TU 的数组,经过数组判别哪个数组更长

这儿咱们先看第一种办法

递归法

可以选用递归来完结,前面咱们也有说过了,数组的很容易爆掉,可是测试用例还算温顺,这题能过

  • 思路是拿一个新数组,和T,U进行比照,哪个先追上新数组的长度,哪个就小
  • 简略一点来说便是,两个不相同长的木棍放在一起,咱们从一端开始不断往前走,先摸到的那个木棍便是短一点的

看到详细完结上

经过引进新的变量 R extends any[] = [] ,来进行辅佐的核算,接着依次判别 TUR['length] 是否持平,这时候,假如 TR['length] 持平了而 U 还没有持平,那就说明晰 T < U ,假如都不持平,那就继续加大数组 R 的长度

怎样加大数组 R 呢?

递归的时候,往数组中多加一个值即可,GreaterThan<T, U, [...R, 1]> 这儿的 1 便是塞进去把 length 整大的

这样就完结了这道巨细判别

// 答案
type GreaterThan<T extends number, U extends number, R extends any[] = []> = 
  T extends R['length']
    ? false
    : U extends R['length']
      ? true
      : GreaterThan<T, U, [...R, 1]>

结构数组

咱们还有一种很巧妙的办法

先看几段小代码

[1, 1, 1, 1] extends [1, 1] ? true : false // false
[1, 1] extends [1, 1, 1] ? true : false // false

上面的例子中,两个数组长度不等,很明显都会返回 false

那么咱们抽象一下,这同样会返回 false,因为很明显多了 ...any

// 伪代码
A extends [...A, ...any] ? true : false

那么咱们就可以有这样的思路,例如比较 2 和 3

咱们就可以比较数组 A[1, 1] 和数组B [1, 1, 1]

咱们就可以这么来看,数组 B 可以表明为 [...A, 1] 所以 B 大于 A,A 没有办法这样表明 B 所以更小

明显咱们这个思路没啥问题!

怎样完结呢?关键在于怎样结构长度为 T 和 U 的数组

写一个生成长度为 N 的数组的办法,需求接受长度 T,还需求运用辅佐变量 A 来保存当前的数组并作为返回值

选用递归的方式往数组中添加新的元素,这样数组的 ['length'] 就会不断的变长,当等于 T 时,就完毕循环,返回数组 A

type newArr<T extends number, A extends any[] = []> =
  A['length'] extends T
    ? A
    : newArr<T, [...A, '']>

验证一下,没啥毛病

type A = newArr<4> // type A = ["", "", "", ""]

TS 类型体操还能这么玩,太秀了!

接下来就好办了,咱们比较这两个数组就好了,为了美观抽出一个 type 来,这个便是咱们前面讲到的逻辑

type GreaterArr<T extends any[], U extends any[]> = U extends [...T, ...any] ? false : true

最后调用它进行比较,KO

type GreaterThan<T extends number, U extends number> = GreaterArr<newArr<T>, newArr<U>>

二、模版字符串随心所欲

这一节,来看看模版字符串在 TS 里有多骚

这一题是 3326 BEM style string,咱们需求完结 BEM 函数完结其规则拼接,不睬他,直接看用例

type ClassNames1 = BEM<'btn', ['price']> // 'btn__price'
type ClassNames2 = BEM<'btn', ['price'], ['warning', 'success']> // 'btn__price--warning' | 'btn__price--success'
type ClassNames3 = BEM<'btn', [], ['small', 'medium', 'large']> // 'btn--small' | 'btn--medium' | 'btn--large'

不过便是根据参数的方位,用不同的符号进行衔接,例如BEN<'aaa', ['b'], ['c']>

b 是第二个参数,那么就用 __ 来衔接,c 是第三个参数就用 -- 来衔接

这题怎样做呢,咱们只需求根据不同的参数运用模版字符串定义不同的模版即可

可是你会发现咱们传参是数组方式的,咱们要的是一个个的,那就需求经过下标来将数组或者目标转成联合类型

// 数组转联合
T[number]
// 目标转联合
Object[keyof T]

特别的,当字符串中经过这种方式申明时,会自动生成新的联合类型,例如这题下面的写法,

type BEM<B extends string, E extends string[], M extends string[]> = `${B}__${E[number]}--${M[number]}`

会得到 type A = "btn__price--warning" | "btn__price--success" 这样的成果

可是这并没有考虑到空数组的状况,因此需求做提前的判别,

type IsNever<T> = [T] extends [never] ? true : false
type IsUnion<U> = IsNever<U> extends true ? "" : U
type BEM<B extends string, E extends string[], M extends string[]> = `${B}${IsUnion<`__${E[number]}`>}${IsUnion<`--${M[number]}`>}`

模版字符串想拼啥就拼啥,酷!

三、中序遍历 TS 也能行

JS 完结中序遍历,你闭着眼睛就能写,那 TS 呢

这是咱们的测试用例,咱们需求完结 InorderTraversal 办法,来完结中序遍历

const tree1 = {
  val: 1,
  left: null,
  right: {
    val: 2,
    left: {
      val: 3,
      left: null,
      right: null,
    },
    right: null,
  },
} as const
type A = InorderTraversal<typeof tree1> // [1, 3, 2]

这题看上去很难,TS 怎样还能遍历树呢,其实是可以的,十分简略,和 JS 的思路是共同的,咱们先看看 JS 是怎样完结中序遍历的呢?

const inorderTraversal = (root) => {
  if(!root) return []
  const res = []
  while(root) {
    inorderTraversal(root.left)
    res.push(val)
    inorderTraversal(tree.right)
  }
  return res
}

JS 是在 root 为 null 时完毕。关于 TS 来说,完结递归,需求 extends TreeNode 而不是 null 来完毕

不能运用 null 来判别,是因为 TS 不能判别类型 T 是否契合 TreeNode 类型

在完毕前,咱们需求递归的调用这个办法,左中右的顺序

// 答案
interface TreeNode {
  val: number
  left: TreeNode | null
  right: TreeNode | null
}
type InorderTraversal<T extends TreeNode | null> = 
  [T] extends [TreeNode] 
    ? (
      [
        ...InorderTraversal<T['left']>,
        T['val'],
        ...InorderTraversal<T['right']>
      ]
    )
    : []

四、infer + 递归随意秒杀

infer 可谓是 TS 中的大杀器,大多数标题都会涉及到它的运用,他可以很方便的帮咱们推断出一个变量的类型,咱们看看这道题

完结一个像Lodash.without函数相同的泛型Without<T, U>,它接纳数组类型的 T 和数字或数组类型的 U 为参数,会返回一个去除 U 中元素的数组 T。

type Res = Without<[1, 2], 1> // expected to be [2]
type Res1 = Without<[1, 2, 4, 1, 5], [1, 2]> // expected to be [4, 5]
type Res2 = Without<[2, 3, 2, 3, 2, 3, 2, 3], [2, 3]> // expected to be []

不必看标题啦,直接看用例,无非便是把第二个参数中的值,从数组中去掉

这题咱们十分容易想,经过 infer 和 递归来完结,用 infer 取出数组的第一项

  • 假如可以被 U 包括,那就丢弃,也便是把剩余的递归,不保留这一项
  • 假如不包括,那就用 [R, ...] 把它给留下,剩余的继续递归 因此很有可能写下这样的代码
type Without<T, U> =
  T extends [infer R, ...infer F]
    ? R extends U
      ? Without<F, U>
      : [R, ...Without<F, U>]
    : T

可是发现只过了一个用例,问题在于 U 有可能是数组,也有可能是字符串,而单纯选用 extends 来判别只能处理字符串的状况

因此咱们需求解决怎样判别字符串和数组两种状况

可以选用数组转 Union 的办法来解决

type ToUnion<T> = T extends any[] ? T[number] : T
type B = ToUnion<['1','b']> // type B = "1" | "b"

这样无论是数字仍是数组,都会转成联合类型,而联合类型很方便判别 extends 包括联系:

// 答案
type ToUnion<T> = T extends any[] ? T[number] : T
type Without<T, U> = 
  T extends [infer R, ...infer F]
    ? R extends ToUnion<U>
      ? Without<F, U>
      : [R, ...Without<F, U>]
    : T

总结

这篇文章经过几道例题,带大家领略了 TS 的风貌,也看到了 TS 的弊端:核算才能,在运用 TS 过程中,咱们要

  • 巧用辅佐变量
  • 遇到核算时斗胆运用 ['length']
  • 怪异字符串操作多用模版字符串
  • infer + 递归大杀器

好了本文的内容就这么多了,更多关于 TS 的内容,今后再说,随缘更新!

有协助的话留言点赞哦!