TS类型体操,看懂你就能玩转TS了

本文以 Typescript 4.5 及以上版本为基础,于 2022年02月07日在掘金首发

本文要实现一种类型工具

type result = Add<\”9007199254740991\”, \”9007199254740991\”>

能计算出两个数字字符串类型的和,即 \”18014398509481982\”

作为一名花里胡哨的前端,看到最近这不是冬奥会开始了,咱们得为运动健儿们呐喊助威,还想着能不能当一次前端届的体操运动员,和奥运健儿们一起感受在冬天运动的魅力

下面,我们就开始做操吧!(如果你不想看这么多,欢迎到评论区点在线尝试的链接去尝试一下吧)

条件类型,条件类型冒号左边为 if 右边为 else

type Example = A extends B ? true : false 中的 truefalse 即可以理解成它们分别为 if 分支和 else 分支中要写的代码

if 中的条件即为 A extends BA 是否可以分配给 B

要实现 else if 则需要多个这样的条件类型进行组合

模式匹配是我们要利用的最有用的 ts 特性之一,之后我们要实现的字符串的增删改查和元组的增删改查都要基于它

如果你想知道更多,可以参考这篇文章:模式匹配-让你 ts 类型体操水平暴增的套路

关于条件类型中 infer 的官方文档:Inferring Within Conditional Types

基于条件类型可以轻松实现与或非

ts 目前不支持动态个数的泛型参数,因此如果有多个条件,我们需要定义多个不同的,比如

现在,我们已经封装了若干个类型工具 And Or Not,要达成基于 ts 的类型系统实现加法器的目标

我们需要很多个这样的类型工具

为了方便管理,我们需要给它分模块,比如上面的与或非,我们划分在 common

我们还需要 function、array、number、object、string 这另外的五个,用于处理函数类型、元组类型、数字类型、对象类型、字符类型

在 js 的运算操作符中,有 =====

在 ts 类型系统中,也可以实现类似的判断

CheckLeftIsExtendsRight 即校验左侧类型是否可分配给右侧类型,和 == 不同的是,== 会进行类型转换后的比较,而条件类型 Left extends Right ? xxx : xxx 只会进行结构性兼容的检查

虽然两个类型长的不一样,但是可以通过约束校验

IsEqual 参考 github – typescript issue:[Feature request]type level equal operator

要实现 ts 的数学运算过于麻烦,因为数字是无法进行 infer 的,如何判断它(一个数字类型)为整型,浮点型?还是正数,或者负数?是不是仅仅有一个数字类型没有任何办法?

这都需要基于字符类型(或元组类型)的模式匹配

效果

在 js 中,我们可以通过 for、while、do…while 等循环进行可迭代对象的遍历,这些都离不开一个东西,那就是循环条件

比如在 for 循环中 for (初始化; 条件; 循环后逻辑) 我们一般用一个变量 i ,每一次循环后进行自增,循环条件一般是将 i 与另一个数比较大小

那么我们还需要实现一个数字类型大小比较,数字类型累加的工具类型

ts 中的循环可以通过递归来实现,ts 的类型系统中不存在类型赋值的概念

只有通过每次递归时,把当前泛型参数处理后,当做下一次递归的泛型参数,终止递归时,返回当前某一个泛型参数的类型

通过一个最简单的递归类型例子来看一下这个过程

如上示例,Result 得到的类型是 [1, 1]

第一次:C 是默认类型 true, 则会走到 Example<false, […Tuple, 1]>,其中 […Tuple, 1] 的结果为 […[1], 1],即 [1, 1]

第二次:C 传入了 false,会走到 Tuple,Tuple 的值为上次传入的值 [1, 1],最后的返回类型为 [1, 1]

除了递归,还有两种方式可以循环,一种是分布式条件类型,还有一种是映射类型,但是它们都很难传递类型

在部分场景下,我们可以兼容 number 类型的数字,也可以兼容 string 类型的数字,定义其为 NumberLike

判断为 0

在上面 循环 章节,我们讲到了,可以通过递归传递修改后的泛型参数,来创建复杂的工具类型

此场景下,我们可以生成动态的类型,最常见的有这几种,元组类型,模板字符串类型,联合类型

而元组类型的长度是可以访问的 如 [0, 1, 2][\’length\’] 结果为 3,且元组类型是可以拼接的,如 […[0, 1, 2], …[0]][\’length\’] 的长度为 4

那么我们可以动态生成两个指定长度的元组类型,然后拼接到一起,获取拼接后的元组长度,就可以得到正整数(和 0)的加法了

参考:juejin.cn/post/705089…

如果想要实现元组类型的排序,那就必须要能够比较数字大小

如何实现数字类型大小的比较呢?

还是得基于元组

基于两个数 N1N2,创建不同的元组 T1T2,依次减少两个元组的长度(删除第一位或最后一位),当有一个元组长度为 0 时,就是这个元组对应的数字类型,比另一个数字类型小(或相等,所以也要先判断是否不相等才进行比较)

去掉数组最后一位的实现:juejin.cn/post/704553…

基于模式匹配,匹配出最后一项,和剩余项,并返回剩余项

类型系统中没有改变原类型的概念,因此元组类型的增删改查都应该直接返回修改后的类型,而不是修改后的变化值

如在 js 中,[1, 2, 3].shift() 会返回 1[1, 2, 3].pop() 会返回 3,但是 ts 类型系统中,这样返回是没有意义的,Pop<[1, 2, 3]> 应该得到类型 [1, 2]

两个数字类型相减的逻辑与两个数字类型比较大小的逻辑类似,但是返回类型时,会返回剩余长度多的元组的长度

这个实现受到元组类型长度的限制,只能得到正数(或 0),即结果的绝对值

且用在其他工具类型中时,会出现 类型实例化过深,并可能无限(Type instantiation is excessively deep and possibly infinite) 的报错,参考 github issue

即:目前有 50 个嵌套实例的限制,可以通过批处理规避限制 (20210714)

减法实现

虽然有嵌套深度的限制,写好的减法不能用,但是加法是很好用的,有加法我们一样可以写出很多逻辑

工具类型可能相互依赖,如果遇到没见过的,请跳转到对应章节查看

原理:TS 内置的模板字符串类型

原理:通过模板字符串类型的模式匹配,使用 GetCharsHelper 匹配出字符串类型的第一个字符和剩余字符,然后将剩余字符继续放入 GetCharsHelper 中进行处理

每次匹配的结果通过 Acc 参数传递,当 S 为空字符串时,S 不能分配给 ${infer Char}${infer Rest},走到 false 分支中,结束递归,即返回 Acc 类型

原理:分割字符串类型,是把字符串类型转为元组类型,参数中需要设置一个元组类型用作返回结果

模板字符串类型的模式匹配是从左往右的,如果字符类型 S\’1,2,3\’${infer Char}${\’,\’}${infer Rest}Char 即为 \’1\’Rest 即为 \’2,3\’,同理,如果 S\’2,3\’${infer Char}${\’,\’}${infer Rest}Char 即为 \’2\’Rest 即为 \’3\’

这样的话,我们只需要把每次匹配出的 Char 放到元组类型参数 T 中的最后一项,在匹配结束后,返回 T 的类型即可

注:array.Push 见下文

原理:元组的长度是可以获取的,通过上文的 Split 可以将字符串类型按照 \’\’ 分割成元组类型,再取元组的 length 即为字符串类型的长度

原理:元组类型可以进行索引访问,可以将字符串类型按照 \’\’ 分割成元组类型,然后通过 索引访问,得到索引位 I 处的字符

原理:TS 模板字符串类型用法

原理:模式匹配可判断字符串类型中是否含有子串

原理:模式匹配时,左侧不写 infer Left,代表左侧只包含空字符串,不存在任何有长度的子串,即 StartsWith

原理:模式匹配时,右侧不写 infer Right,代表右侧只包含空字符串,不存在任何有长度的子串,即 EndsWith

原理:匹配出 ${infer Left}${S2}${infer Right}Left,求其长度,则索引位即为 Left 的长度,如果匹配不到,返回 -1

可以先比较父串和子串的长度,如果子串比父串还长,那就不需要匹配了,直接返回 -1

原理:模板字符串类型的模式匹配是从左往右的,而 LastIndexOf 是从右往左的,所以在匹配时,仍然基于从左往右匹配,但是每次匹配后,替换掉匹配过的子串为空串

然后把删掉的部分的长度累计起来,结果就是模拟从右往左匹配到的索引值

注:Replace 见下文

原理:基于模板字符串的模式匹配,匹配到了就用 ReplaceStr 换掉 MatchStr

原理:基于 Replace,递归进行替换,替换掉所有 MatchStr,终止条件是 S 是否包含 MatchStr

原理:当重复次数 Times0 时,直接返回空字符串

在参数中传递循环条件 Offset (每次传递时加 1,即 number.IntAddSingle<Offset, 1>),当循环条件 Offset 和循环次数 Times 相等时,结束递归

每次递归中,都在字符串的起始位置插入一个字符串 S,即

注:number.IntAddSingle、number.IsEqual 见下文

原理:比较指定的长度和当前字符串类型的长度相等,如果满足长度,直接返回 S,每次递归时,给 S 左侧添加指定的字符,直到 S 的长度满足指定的长度时,终止递归

原理:比较给定的长度和当前字符串类型的长度相等,如果满足长度,直接返回 S,每次递归时,给 S 右侧添加指定的字符,直到 S 的长度满足给定的长度时,终止递归

原理:每次匹配 ${一个空格}${剩余字符} 然后让 剩余字符 继续匹配,直到不符合 ${一个空格}${剩余字符} 的规则时,终止递归,返回 S

原理:每次匹配 ${剩余字符}${一个空格} 然后让 剩余字符 继续匹配,直到不符合 ${剩余字符}${一个空格} 的规则时,终止递归,返回 S

原理:先用 TrimRight 去掉右侧的空格,再把前者结果交给 TrimLeft 去掉左侧的空格

原理:TS 内置

原理:TS 内置

原理:遍历字符串类型的每一个字符,如果当前索引大于等于 Start,并且小于等于 End,就把当前字符 push 到元组中,最后用 array.Join,将元组转为字符串类型

注:array.Join 见下文

原理:SubString 需要起始和结束,有 StartLen 就可以先算出 End,就可以使用 SubString

原理:遍历元组类型,如果 Offset 等于给定的索引,则该索引对应的类型替换为给定的类型,否则用原类型

原理:元组(数组)类型的索引访问会得到联合类型

原理:基于元组的模式匹配,提取最后一项,返回剩余项

原理:与 Pop 同理

原理:[] 中直接写类型可以构建新元组类型,其中写 …Tuple,与 js 中的扩展运算符效果一致

原理:同 UnShift

原理:见 UnShift

原理:每次递归时提取元组第一个类型,然后将此类型放到模板字符串类型的第一个位置 ${第一个位置}${第二个位置}${第三个位置}

第二个位置即转为字符串用来分隔的子串,如果元组的长度为 0,则为空串

第三个位置则是剩下部分的逻辑,即重复最开始的逻辑

原理:初始类型 CacheBool 为 true,依次将元组中每个类型与初始类型进行 操作,如果元组长度为 0,则返回 false

注:common.And、common.CheckLeftIsExtendsRight 见下文

原理:初始类型 CacheBool 为 false,依次将元组中每个类型与初始类型进行 操作,如果元组长度为 0,则返回 false

注:common.Or、common.CheckLeftIsExtendsRight 见下文

原理:如果原元组长度为 0,则直接返回由新类型构成的元组 F[]

如果是数组类型如:any[]never[]number[],也应该直接替换成 T[]

否则,每次在原元组中删除第一个,然后在最前面添加一个新类型,直到循环条件与 T 的长度一致时,终止递归

注:commom.IsEqual 见下文

原理:严格模式,即 any 只能为 any,而不能为 1unknown 这样的其他类型

如果是严格模式就用 common.IsEqual 进行约束校验,否则用 common.CheckLeftIsExtendsRight 进行约束校验

每次递归时,如果满足上述条件,则放入新的元组类型中

如果循环条件 Offset 等于 T 的长度是,终止循环,返回新的元组类型 Cache

注:common.Not 见下文

原理:声明一个接口用于构造新的元组类型中的项,然后每次递归都向 Cache 中添加一个经过 IndexMappedItem 处理后的类型

注:由于 TS 中实现不了回调的效果,因为带泛型参数的工具类型不能直接当做类型传递,必须要先传了泛型参数才能用,所以暂时无法实现 jsArray.prototype.map 的效果

原理:遍历在元组中找,如果找到了匹配的,则返回该类型,否则返回 null 类型

原理:遍历老元组类型,每次在新元组类型 Cache 的前面插入当前类型

原理:反转老元组类型,然后通过 Find 查找

原理:严格模式,参考上文 Filter,遍历元组,符合约束校验时,返回当前 Offset,否则结束后返回 -1

原理:通过 MapWidthIndex 将索引记录到元组的每个类型中,使用 Find 匹配反转后的元组,匹配到时,返回该类型的 Item[\’index\’] 值即为结果

原理:遍历元组类型,如果当前类型不满足 unknown[] 的约束,则将它 Push 进新元组中,否则将它 Concat 进去

原理:将元组转为联合类型,如果约束条件 C 可以分配给该联合类型,那么就是 true

原理:和字符串裁剪的类似,遍历老元组,当循环条件 Offset 大于等于 Start 或 小于等于 End 时,将这些类型 Push 到新元组中

原理:最简单的冒泡排序,每次排序时,将大的与小的置换

注:受到嵌套实例深度的限制,只能排两个类型的元组

注:原理见上文,NumberLike 见上文

注:原理见上文

原理:转为字符串类型后判断是否小数点

原理:IsFloat 取反

原理:见上文

原理:IsEqual 取反

原理:见上文

原理:见上文

原理:见上文

原理:循环,当 Offset + Offset 等于 N 时,或 Offset + 1 + Offset 等于 N 时,Offset 即为结果

原理:创建一个字符与元组的映射表,我们通过前文的探究已知数字类型可由元组的长度得到,那么就根据每个字符依次构建不同长度的元组即可得到结果

Make10Array 是将上一次的结果 * 10

见下文

让我们回忆一下,在小学三年级的数学计算中,10进制的小数相加要怎么做?

1.8 + 1.52

是不是要先把小数点对齐

然后从右往左依次计算,如果当前位的结果大于等于 10,则需要往左边进一位

那么 ts 要如何知道 1 + 1 = 2,1 + 2 = 3 呢?

我们可以定义一个映射表,二维元组,用索引访问的方式得到一位整数加法的结果和它的进位情况

\”0.1\”\”1\” 即是,用 ts 表示即 ${number}

但是这样的数字也是符合这个条件的:\”000.1\”

如果想限制这样的数,用正则表达式很好处理,如何在 ts 类型系统中限制呢?

我们可以定义除了小数点前面有多个零的情况的类型

我们如果是 1 + 1,且加法表为 AddMap,我们想这样使用 AddMap[1][1],得到相加的结果和需要进位的结果

即:

小学三年级数学中的加法,要从右往左算,要以小数点对齐

但是在上文中我们实现的字符串或元组处理工具中,从右往左的都很麻烦,所以从左往右更简单,且元组操作要比字符串操作更方便,那么在实际的加法运算中

我们真实处理的数据应该是一个被反转的元组

  1. 按小数点对齐

即按小数点分割,然后用 PadStart 和 PadEnd 补 0

  1. 转元组后反转方便从左往右计算

单独计算小数位或整数位,减少复杂度,如果有进位,就在元组的最前面添加 \”10\”

数组和对象方法&数组去重

  • arr.concat(arr1, arr2, arrn);–合并两个或多个数组。此方法不会修改原有数组,而是返回一个新数组

  • arr.fill(value,start,end) ;–用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。此方法修改原有数组

  • arr.filter((element,index,array)=>{},this) ;–filter方法过滤数组,返回一个新数组, 其保留 通过所提供函数测试的 所有元素。此方法不会修改原数组

  • arr.reduce((previousValue,currentValue,index,array) => {},initialValue);–reduce方法迭代数组的所有项,又称为“累加器”,返回函数最终计算的一个值。

  • arr.join(\”,\”);–将一个数组的所有元素连接成一个字符串返回。不会修改原有数组

  • arr.sort();–对数组的元素进行排序,并返回数组。此方法修改原有数组

  • arr.unshift(e1, e2, en);–添加元素到数组的头部,返回该数组的新长度。此方法修改原有数组

  • arr.push(e1, e2, en);–添加元素到数组的尾部,返回该数组的新长度。此方法修改原有数组

  • arr.pop();–删除数组尾部的元素。此方法修改原有数组

  • arr.shift();删除数组头部的元素。此方法修改原有数组

  • arr.splice(index, count);–删除任意位置元素的方法。此方法修改原有数组

  • arr.reverse();–将数组中元素的位置颠倒,并返回该数组。此方法修改原有数组

  • arr.slice(start, end);–包头不包尾的截取数组中的一段,并返回新数组。不会修改原有数组

  • arr.splice(index, count, e1, e2, en);–添加元素到数组的任何位置。此方法修改原有数组

  • arr.indexOf(searchElement,fromIndex);–返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。

  • arr.includes(searchElement,fromIndex); // ES6 –判断一个数组是否包含一个指定的值,根据情况,返回布尔值。

  • arr.map((currentValue,index,array) => {} ,this);–创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。此方法修改原有数组

  • arr.forEach((currentValue,index,array) => {} ,this);–对数组的每个元素执行一次给定的函数。

  • arr.from(arrayLike,(currentValue) => {} ,this);–从一个类似数组或可迭代对象创建一个新的数组,浅拷贝的数组实例。此方法修改原有数组

  • arr.find((element,index,array) => {} ,this);–返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。find方法不会改变数组

  • arr.findIndex((element,index,array) => {} ,this);— 返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。findIndex不会修改所调用的数组

  • arr.flat(depth);–会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。此方法不会修改原有数组

  • arr.flatMap((currentValue,index,array) => {} ,this);–首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。与 map 连着深度值为1的 flat几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。此方法不会修改原有数组

  • arr.some((element,index,array)=>{},this)–依据判断条件,数组的元素是否有一个满足,若有一个满足则返回ture

  • arr.every((element,index,array)=>{},this)–依据判断条件,数组的元素是否全满足,若满足则返回ture

  • Object.assign(); –将所有可枚举属性的值从一个或多个源对象分配到目标对象,它将返回目标对象

  • Object.defineProperty() ;–直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

  • object.hasOwnProperty(prop);–对象自身属性中是否具有指定的属性(返回布尔值)

  • Object.getOwnPropertyNames();–返回一个由指定对象的所有自身属性的属性名组成的数组

  • object.propertyIsEnumerable(prop);–判断指定的属性是否可枚举(返回布尔值)

  • object.valueOf();–返回指定对象的原始值

  • object.toString();–返回一个表示该对象的字符串Object.prototype.toString.call()

  • Object.create();–创建一个新对象,使用现有的对象来提供新创建的对象的proto。

  • Class.prototype.isPropertyOf(object);–测试一个对象是否存在于另一个对象的原型链上

  • Object.keys();–方法会返回一个由一个给定对象的自身可枚举属性组成的数组

  • Object.values();–方法返回一个给定对象自身的所有可枚举属性值的数组

  • Object.entries();–方法返回一个给定对象自身可枚举属性的键值对数组

  • Object.setPrototypeOf();–方法设置一个指定的对象的原型

  • Object.getPrototypeOf();–方法返回指定对象的原型

arr.slice(开始位置(含), 结束位置(不含)):“读取”数组指定的元素,不会对原数组进行修改;

arr.splice(index, count, [insert Elements]):操作”数组指定的元素,会修改原数组,返回被删除的元素;

index :是操作的起始位置

count = 0 :插入元素,count > 0 :删除元素;

[insert Elements] :向数组新插入的元素;

  • 方法二、利用for嵌套for,然后splice去重(ES5中最常用)

  • 方法三、利用indexOf去重

  • 方法四、利用sort()

  • 方法五、利用对象的属性不能相同的特点进行去重

  • 方法六、利用includes

  • 方法七、利用hasOwnProperty

  • 方法八、利用filter

  • 方法九、利用递归去重

  • 方法十、利用Map数据结构去重

  • 方法十一、利用reduce+includes

  • 方法十二、[…new Set(arr)]

快速的让一个数组乱序

  • Array.isArray(arr)

  • Object.prototype.toString.call(arr) === \'[Object Array]\’

  • arr instanceof Array

  • array.constructor === Array

转自简书:Angel_6c4e原文链接:https://www.jianshu.com/p/4cfde898264b

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。