likes
comments
collection
share

Ts 类型编程(三)

作者站长头像
站长
· 阅读数 15

从往期的内容看下来,我们会发现关于类型编程的一些情况下或多或少的存在着局限性,就是当数据层级不再单一,需要处理的数组、字符串时,单单通过提取、类型映射是做不到的。

但其实我们在前边往期学习的过程已经在解决这些场景时的局限性问题了,就是:递归

本章看点:

  1. 解决数据层级存在嵌套,数组、字符串复杂度高时,递归的使用方式 。
  2. 在 TS 类型的世界里不支持直接进行数字运算,那该如何去解决这个问题 ?

往期回顾:

  1. TypeScript 类型系统中的类型和类型运算
  2. Ts 类型编程(一)
  3. Ts 类型编程(二)

递归

Ts 类型系统是不支持循环的,所以我们通过递归来实现循环的效果。来看下递归的定义: 通过重复将问题分解为同类的子问题而解决问题的方法,即自调用。

我们来看一个简单的,在第一章中其实我们实现过的:reverse

type reverse<T extends unknown[], Ary extends unknown[] = []> =
    T extends [] ? Ary 
        : T extends [...infer L, infer R] ? reverse<L,[...Ary, R]> // 这里可以自己举个例子看一下      
    : Ary; 
    
type arr = reverse<[1,2,3,4,5,6]>

在这个例子中,我们想实现的是反转数组(也就是说逆序)。我们自然而然的想法便应该是:提取末尾元素放置数组的第一个位置,或者同理说将数组第一个位置的元素提取出来,放置在末尾位置。

在这里我们通过 extends 对数组 T 进行了约束,由于数组类型是未知的,所以在这里将 T 约束为未知类型的数组。同理,我们以同样的方式定义了返回值:Ary extends unknown[]。先判断 T 是否是 [] 的子类型:否则直接返回空数组;是则对 T 进行提取,我们将数组的最后一个元素提取出来并通过递归的方式对剩余数组元素继续进行提取重组,直到所有元素替换完成。

同理,因为涉及到遍历所以很多数组方法都可以通过递归的方式来逐一实现。

再来看 trim 的实现,同样也是在之前便实现过的,可以去看一下:

// 修剪左侧空白字符
type trimL<str extends string> =
str extends `${" " | "\n" | "\t"}${infer res}`
    ? trimL<res> : str;
    
// 修剪右侧空白字符
type trimR<str extends string> =
str extends `${infer res}${" " | "\n" | "\t"}`
    ? trimR<res> : str;

// 嵌套 至于这么做的原因在Ts类型编程(一)有详细说明
type trimStr<str extends string> = trimL<trimR<str>>
// 或者
type trimStr<str extends string> = trimR<trimL<str>>

会发现在修剪两侧的空白字符时我们都不可避免的使用到了递归的方式来修剪。

我们从开始到现在分别做到了删(trim)、改(reverse),可以删改也就可以做查的操作 includes

// 查找元素是否存在,存在返回true 反之则为false
type includes<arr extends unknown[], target> = 
	arr extends [infer origin, ...infer res] 
	? isEqual<origin, target> extends true ? true : false
	: false

type isEqual<origin, target> = (origin extends target ? true: false) & (target extends origin ? true : false)

type resTrue = includes<[1,2,3],1> // true
type resFalse = includes<[1,2,3],5> // false

来看下两次执行的结果:

Ts 类型编程(三)

Ts 类型编程(三)

到现在我们应该已经掌握了递归在Ts中的妙用了,如果Js足够好的话 其实一点就通的。

再来看关于对象类型的递归,其实就是我们在第一章中提到的索引类型

在上一章中,我们知道怎样为索引类型添加修饰符,但当时我们并未去考虑当索引类型的层级嵌套很深的情况,现在我们来解决这个问题:

type o = {
    city: {
        company: {
            person: {
                name: string,
                age: number,
            }
        }
    }
}
// 之前讲到的 为索引类型添加修饰符
type bindReadonly<o extends Record<string, any>> = {
	readonly [k in keyof o] : o[k];
}

type deepReadonly<o extends Record<string, any>> = 
   // o extends any ? 
        {
            readonly [k in keyof o] :
                    o[k] extends object 
                            ? o[k] extends Record<string, any> 
                                    ? deepReadonly<o[k]> : o[k]
                            : o[k]
        } 
   // : never

type res = deepReadonly<o>['city']

Ts 类型编程(三)

Ts 类型编程(三)

查看res会发现:我们需要通过访问的方式去查看内部节点是否的确被添加修饰符,那这是为什么?原因是:Ts为了保证性能上的效率,需要整个类型被用到的时候才会执行其中的计算逻辑。

当我们打开代码行中的注释时去触发计算,则可以看到我们理想中希望的样子:

Ts 类型编程(三)

最后,这里我们仅仅处理了子节点是对象的情况,并没有处理其他数据情况:数组、方法等,感兴趣的可以自行尝试 其实只是多了几行判断而已。

截至到现在,我们要能想到的是在Ts的语法环境下,遇到数量不确定的数据处理的情况下,要学会首先想到的就是使用递归来处理问题。

数值计算

TS 类型的世界里不支持直接进行数字运算,但元组存在 length 属性,可以通过构建元组(我们构建出来的“数组”并不是传统意义上的那种泛泛的而是有具体类型长度的,所以说是元组更合适)来获取长度计算的方式来解决问题。

在学习的过程中,可能会说字符串也是有 length 属性的,但是只有元组的长度才是具体的数字,而字符串则是类型 number

type ary = [1,2,3,4]['length']
type str = 'adfasdfasdf'['length']

Ts 类型编程(三)

知道这些前置知识之后,便可以进行Ts中的四则运算了。在开始之前,我们要做的是如何构建一个任意长度的元组呢,当然是递归了。

type buildAry<
    len extends number,
    elem = unknown, 
    resAry extends unknown[] = []
> = resAry['length'] extends len 
    ? resAry
    : buildAry<len, elem, [elem, ...resAry]>
    
// 构建长度为10的元组
type tuple = buildAry<10, number, []>

Ts 类型编程(三)

接下来便可以逐一实现运算了。

Plus

// 通过构建两个长度的元组,并通过展开运算符展开元素到新数组 最后提取长度即我们最后求得的值
type Plus<x extends number, y extends number> =
	[ ...buildAry<x, number, []>, ...buildAry<y, number, []> ]['length']
       
type plus = Plus<1,2>

Ts 类型编程(三)

Minus

// 通过构建出一个减数长度的元组并将其元素内容展开,再将剩余元素的内容通过infer提取出来 获取其长度即可
type Minus<x extends number, y extends number> = 
	buildAry<x> extends [...n1: buildAry<y>, ...n2: infer res]
	? res['length'] : never
       
type minus = Minus<3,1>

Ts 类型编程(三)

Mult

// 乘法本就是几个几相加得到的,我们通过构建其中一个值的长度的元组,并将其放置再作为返回值数组内
// 每放置一次 便将另一个元素值减一直至为0 最后将作为返回值数组的长度返回即可
type Mult<x extends number, y extends number, resAry extends unknown[] = []> = 
	y extends 0 
	? resAry['length'] : Mult<x, Minus<y, 1>,[...resAry, ...buildAry<x>]>;

type mult = Mult<2, 2>

Ts 类型编程(三)

Div

// 除法在实现上是判断被除数内部包含了多少个除数 我们通过减的方式代表包含 每减一次便在作为返回值的数组内添加一个任意类型的字符作为占位
// 直至做减法到为0未为止 拿到作为返回值数组的长度即可
type Div<x extends number, y extends number, resAry extends unknown[] = []> = 
	x extends 0 
	? resAry['length'] : Div<Minus<x, y>, y, [unknown, ...resAry]>

type div = Div<10,2>

Ts 类型编程(三)

我们知道字符串的length属性返回的是一个number,那我们是不是可以手动的去实现长度的获取呢。当然是可以的:

type strLen<str extends string, count extends unknown[] = [] >
    = str extends `${infer s}${infer res}` 
        ? strLen<res, [unknown, ...count]>
        : count['length']

type s = strLen<'123'>

查看 s 便可以看到我们拿到了字符串的长度:

Ts 类型编程(三)

在前面我们实现了运算和字符串的计数,在最后我们来看如何实现比较两数值的大小:


type compare<x extends number, y extends number, resAry extends unknown[] = []>
    = x extends y 
        ? false 
        : resAry['length'] extends y 
            ? true 
            : resAry['length'] extends x
                ? false
                : compare<x, y, [unknown, ...resAry]>

type res = compare<3,5>
type res2 = compare<3,2>

这样我们便实现了两数值的比较,来看一下对比结果: Ts 类型编程(三)

总结:

在本章我们详细的介绍了递归以及如何做数值计算,并且整个过程也离不开infer做类型的提取。这需要我们对前面所学的知识,有一个很好的掌握。

转载自:https://juejin.cn/post/7204060150704324667
评论
请登录