TS 类型体操 之 数组的用法与进阶(加法,减法,乘法)

TS 类型体操 之 数组的用法与进阶(加法,减法,乘法)

之前《成为优秀的 TS 体操高手 之 TS 类型体操前置知识储备》提到了一下数组的用法,在 TS 中主要担任 计数器 的角色。用到的主要是数组的 length 属性

当时简单的讲了一下 18・获取元组长度。用到的就是 数组的 ['length'] 属性就解决了

今天继续讲讲数组 计数器 的深入用法和进阶用法。用到的例题分别是:

简单的热身:5153・IndexOf 5317・LastIndexOf

了解数组的加法:4182・斐波那契序列

思考进阶的用法:2257・MinusOne

5153 · IndexOf 和 5317 · LastIndexOf 题目详解

这 2 个题目的需求和 JS 的用法一样,就是获取指定内容在数组中的 索引

在开始之前先来个 JS 版实现看看是什么样的代码

function IndexOf(T, U) {
  for (let i = 0; i < T.length; i++) {
    // 1. 循环拿到每一项
    const item = T[i]
    // 2. 判断和U的关系
    if (item == U) {
      // 3. 相同的返回索引就可以了
      return i
    }
  }

  // 4. 如果循环结束了都没匹配到就返回 -1
  return -1
}

换到 TS,由于我们没有 for 循环,想计算索引就必须借助额外的参数(数组 C,默认是[],也就是长度 0),每一次递归调用的时候数组都 push 一个 1,让数组长度来帮我们统计 当前循环的索引

而且因为我们的数组只是为了统计当前索引,所以我都是用 [1,1,1] 这种数组,自然 C extends 1[]

// 半成品答案
type IndexOf<T extends unknown[], U, C extends 1[] = []> =
  T extends [infer F, ...infer R] ? (F extends U ? C['length'] : IndexOf<R, U, [...C, 1]>) : -1

上面的答案只能应对正常类型,下面的 2 个示例答案就不对了

// 答案是:2,程序运行结果是1
var TestCase1 = IndexOf<[string, 1, number, 'a'], number>

// 答案是:4,程序运行结果是0
var TestCase1 = IndexOf<[string, 1, number, 'a', any], any>

因为对于 TestCase1 来说 1 extends number 也是为true
对于TestCase2 来说,不等于 never 的都可以 extends 为 any

关于这个问题,可以看看之前写的 《TS 类型体操 之 extends,Equal,Alike 使用场景和实现对比》。深入了解一下 extends 背后的用法和特性

既然单纯的 extends 不能解决问题,那就直接上 Equal(当然不嫌麻烦也可以用 F extends U && U extends F 的方式,我嫌麻烦就简写了,这样会多了很多的三目运算的判断)。

// 正确答案
export type IndexOf<T extends unknown[], U, C extends 1[] = []> =
  T extends [infer F, ...infer R] ? (Equal<F,U> extends true ? C['length'] : IndexOf1<R, U, [...C, 1]>) : -1

题外话:

注意我加了一个 export ,测试用例使用的时候也得加上 import { IndexOf } from './template'
因为 Equal 是工具类封装的,并非 TS 内置的方法,所以我们需要 import 进来
当这个 TS 文件导入了外部的模块时,你可以理解为当前的 TS 文件也被模块限制了(没 import 之前可以理解为全局)


LastIndexOf 解法

和 IndexOf 相比,LastIndexOf 区别是取到最后一个符合规范的索引,也就是要把数组完全循环完(IndexOf 是找到第一个符合的就立刻returen C['length'] 了)

所以比起 IndexOf:计数器还得有C extends 1[] = [],还得新增一个最后一次匹配的索引 L extends number = -1

要注意的点上面也都提到过了,就多加了一个 L 参数,答案就直接放出来了:

export type LastIndexOf<T, U, C extends 1[] = [], L extends number = -1> = T extends [infer F, ...infer Rest]
  ? Equal<U, F> extends true
    ? LastIndexOf<Rest, U, [...C, 1], C['length']> // 如果匹配到了,L记录为最新的索引
    : LastIndexOf<Rest, U, [...C, 1], L> // 如果不匹配,L还是记录为上次的索引
  : L

数组的加法 - 斐波那契数列

斐波那契数列是这样一个数列:1、1、2、3、5、8、13、21、34
F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
当前的数字 = 前面一位数字 + 前面 2 位的数字

4182・斐波那契序列 的测试用例:

实现的需求,传入一个位置,获取该位置对应的数列结果:

// 数列是: 1、1、2、3、5、8、13、21、34
type Result1 = Fibonacci<3> // 2
type Result2 = Fibonacci<8> // 21

解题思路

  • 既然有指定位置,那 计数器 少不了
  • 斐波那契数列也要实现一个
    • 那么就涉及到 加法,加法的实现其实就是 [...arr1,...arr2]
    • 如果需要加法,那么起码得有 2 个数 相加,题目给出只有一个变量,所以我们还得自己多加 2 个变量

那就是要加 3 个参数,一个计数器,一个是 2 个数的加法

type Fibonacci<T extends number, C extends 1[] = [], N1 extends 1[] = [], N2 extends 1[] = [1]> =
  T extends C['length'] ? N1['length'] : Fibonacci<T, [...C, 1], N2, [...N1, ...N2]>

解题细节:

  1. C 作为计数器(N1,N2 也是计数器),直接限制为元素只有 1 的数组了 所以 C extends 1[] = []
  2. N1 和 N2 对应的是数列上 F(N-2) 和 F(N-1)
  3. 当递归继续运行时,N1 的位置被赋上了 N2 的值;而 N2 = N1+N2。加法就体现在 [...N1, ...N2]

递归内存溢出问题与数组高阶用法

递归虽然好用,TS 的语法除了 in 循环多数都是靠递归了。不过递归有一个很明显的问题就是 内存溢出,看一道 MinusOne 题目的实现就能遇到了

2257 · MinusOne 题目实现

刚才斐波那契数列是加法,而这个是减法,而且只是减 1。只需要合理的利用计数器和比计数器数组多一位的数组,就能很简单的得出相邻的 2 个数字了

看懂这个实现应该没问题,N 在初始的时候就把 C 多一位,所以 N 和 C 的 length 数是相邻的 2 个数(看不懂的还得在看看上面的内容)

type MinusOne<T extends number, C extends 1[] = [], N extends 1[] = [1]> =
  T extends N['length'] ? C['length'] : MinusOne<T, [...C, 1], [...N, 1]>

然而有这么一个 错误(警告)

Type instantiation is excessively deep and possibly infinite.(2589)

类型实例化过于深入,可能无限。(2589)(你递归调用循环次数太多了,我可能会内存溢出)

经过不严谨的测试,我发现只要数字大于等于 1001 就会有这个报错了

数组的另类实现方法(乘法)

如果循环太多会导致内存溢出,那我们只能想办法减少循环。

假设我们只需要一个长度为 11 的数组
思考一下除了逐 1 递增(递归 10 次),还有什么公式可以快速实现?

观察下面的算式(需要亿点点小学知识)

11 = 1+1+1+…+1
11 = (1 * 10) + 1

103 = 1+1+1+1+1…+1
103 = (1 * 10 * 10) + (0 * 10) + 1 + 1 + 1

只需要把枯燥的加法,升级为乘法,十位数乘 10,百位数乘 100,原先一屏幕都写不下的 +1 就被几个 * 10 轻松搞定

Q:TS 如何实现乘法?
A:乘法无非就是把自己放大多少倍,那么 TS 的乘法可以理解为: [...T,...T,...T]['length'] 相当于 T['length'] * 3

在看上面的 103 ,个位数剩下多少数字也还得相应的加多少次,讲道理我到 100 我才加了 2 次就完事了,为了个位数,还得加 3 次(递归 3 次),显然不划算

对于这种有限情况(个位数无非就是 0-9 10 种情况),完全可以自己列出来。程序需要3,我就给你返回 [1,1,1] 一步到位

  • 把 0-9 的情况列出
type N = {
  '0': []
  '1': [1]
  '2': [1, 1]
  '3': [1, 1, 1]
  '4': [1, 1, 1, 1]
  '5': [1, 1, 1, 1, 1]
  '6': [1, 1, 1, 1, 1, 1]
  '7': [1, 1, 1, 1, 1, 1, 1]
  '8': [1, 1, 1, 1, 1, 1, 1, 1]
  '9': [1, 1, 1, 1, 1, 1, 1, 1, 1]
}

type Number3 = N['3'] // [1,1,1]
type Number9 = N['9']['length'] // 9

Q: 为什么要返回数组?
A: 返回数组相对来说更加灵活。如果某段程序需要计数器直接从 10 开始计算,返回 10 的数组能省下前面的 10 次计数器的循环;如果想拿纯数字,直接基于数组获取 [‘length’] 属性就可以了。

0-9 解决后,思考 十位数 * 10;百位数 * 10 * 10 的方案

上面也提到了,乘法无非就是 [...T,...T,...T]。乘 10,只需要 10 个 ...T(10 个而已,还属于有限情况哈)

把 N 改造下(每一项多加 10 个 ...T):

type N<T extends 1[] = []> = {
  '0': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
  '1': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1]
  '2': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1]
  '3': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1]
  '4': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1, 1]
  '5': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1, 1, 1]
  '6': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1, 1, 1, 1]
  '7': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1, 1, 1, 1, 1]
  '8': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1, 1, 1, 1, 1, 1]
  '9': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}

看到这里还是很蒙,传入 T 数组有何作用?结合下面的 CountToT 思考一下

type CountToT<T extends string | number, Count extends 1[] = []> =
  `${T}` extends `${infer First}${infer Rest}` ? CountToT<Rest, N<Count>[keyof N & First]> : Count

type Number11 = N<[1]>[1] // 11
type Number53 = N<[1,1,1,1,1]>[3] // 53

CountToT 作用解释
假设现在需要生成一个 103 长度的数组,作为 TS,我们需要把这个分成 3 个数 1,0,3 (字符串的 infer 用法)

  • 程序运行后 First 拿到数字 1 后
    • N<Count>[keyof N & First]> Count 目前是 [],而 First 则是 1,所以拿到的结果是 [1]
    • 进行下一次调用的时候 T 就会拿到 03,而 Count 拿到的就是 [1]
  • 第二次 First 则是拿到了 0
    • N<Count>[keyof N & First]> Count 目前是 [1],而 First 则是 0,结合 N 的实现,[1] 会被放大 10 倍,然后在加上 0 的值。所以 N<Count>[keyof N & First]> 得到的是长度为 10 的数组 (看懂这一步 10 怎么得出来的很重要!)
    • Rest 就剩下 3,继续递归调用
  • 第三次 First 拿到 3 ,而 Count 是 长度为 10 的数组
    • N<Count>[keyof N & First]> 这时候执行的时候,会把 10 长度的 放大 10 倍,然后在加上 3 个 1 的数组,就实现了最终结果:长度为 103 的数组

CountToT 的巧妙地点在于是逐个拿到数字,然后每拼接一个新的数字,前面的数对应要 * 10

回到 2257・MinusOne 题目

T 传入的就已经是数字(这时候我们就能直接生成对应的数组长度了,而且大幅减少递归的次数了),剩下的只需要考虑减 1 的问题

至于减 1,我们就要用到 infer 了。在 TS 前置储备知识的时候就说过 对于字符串的 infer 是特别灵活的,他会自动根据你固定有占位的内容算出来,在分配其他内容给 infer


// type N 的实现 ...
// type CountToT 的实现 ...

// 上面说的2个实现也要的哈,就不复制下来了

type MinusOne<T extends number> =
  CountToT<T> extends [...infer M1, 1] ? M1['length'] : 0

核心在于 [...infer M1, 1]。假设 CountToT<10> 生成了一个 长度为 10 的数组([1,1,1…,1])。而 [...infer M1, 1] 后面已经占了一个 1,那么根据 ... 运算符M1 就会只包揽前 9 个 1 !!(同理,如果改为 [...infer M1, 1 ,1],那么 M1 的长度就是8!)

实现一个 TS 的减法,不仅仅只是减 1

那么减 1 实现了,能不能实现一个 减法 ?!



// type N 的实现 ...
// type CountToT 的实现 ...

type Minus<T extends number, M extends number, TA extends 1[] = CountToT<T>,MA extends 1[] = CountToT<M>> =
  TA extends [...infer M1, ...MA] ? M1['length'] : MA extends [...infer M2,...TA] ? `-${M2['length']}` : 0

type numberMinus = Minus<100,46> // 54  (数字类型)
type numberMinus = Minus<46,100> // -54  (字符串数字类型)

动态的减法,因为数组并不存在负长度的数组,所以只能拼成字符串了。

思路跟整篇文章是一样的,不过我加了 TAMA 的变量,因为下面比较经常会用到这 2 个变量,没必要每次比较都动态生成一次数组,太浪费,直接起新变量存起来用就行

最后

从数组最一开始的逐 1 递增的计数器;到 斐波那契数列 的 数列加法;研究了数组的 乘法 用于快捷生成百位,千位数的数组,以及结合 infer 的超强匹配实现 TS 减法。对 TS 的数组使用理解更加深刻了,至此,数组用法的探索就到这里了,也是个人对 TS 数组用法理解的一个瓶颈了。

当然对于 TS,或者对于数组来说,肯定还有更多的玩法可以发掘,欢迎一起讨论学习~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值