TS类型体操,看懂你就能玩转TS了
本文以 Typescript 4.5 及以上版本为基础,于 2022年02月07日在掘金首发
本文要实现一种类型工具
type result = Add<\”9007199254740991\”, \”9007199254740991\”>
能计算出两个数字字符串类型的和,即 \”18014398509481982\”
作为一名花里胡哨的前端,看到最近这不是冬奥会开始了,咱们得为运动健儿们呐喊助威,还想着能不能当一次前端届的体操运动员,和奥运健儿们一起感受在冬天运动的魅力
下面,我们就开始做操吧!(如果你不想看这么多,欢迎到评论区点在线尝试的链接去尝试一下吧)
条件类型,条件类型冒号左边为 if 右边为 else
type Example = A extends B ? true : false 中的 true 和 false 即可以理解成它们分别为 if 分支和 else 分支中要写的代码
而 if 中的条件即为 A extends B,A 是否可以分配给 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…
如果想要实现元组类型的排序,那就必须要能够比较数字大小
如何实现数字类型大小的比较呢?
还是得基于元组
基于两个数 N1、 N2,创建不同的元组 T1、T2,依次减少两个元组的长度(删除第一位或最后一位),当有一个元组长度为 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
原理:当重复次数 Times 为 0 时,直接返回空字符串
在参数中传递循环条件 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 需要起始和结束,有 Start 和 Len 就可以先算出 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,而不能为 1、unknown 这样的其他类型
如果是严格模式就用 common.IsEqual 进行约束校验,否则用 common.CheckLeftIsExtendsRight 进行约束校验
每次递归时,如果满足上述条件,则放入新的元组类型中
如果循环条件 Offset 等于 T 的长度是,终止循环,返回新的元组类型 Cache
注:common.Not 见下文
原理:声明一个接口用于构造新的元组类型中的项,然后每次递归都向 Cache 中添加一个经过 IndexMappedItem 处理后的类型
注:由于 TS 中实现不了回调的效果,因为带泛型参数的工具类型不能直接当做类型传递,必须要先传了泛型参数才能用,所以暂时无法实现 js 中 Array.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],得到相加的结果和需要进位的结果
即:
小学三年级数学中的加法,要从右往左算,要以小数点对齐
但是在上文中我们实现的字符串或元组处理工具中,从右往左的都很麻烦,所以从左往右更简单,且元组操作要比字符串操作更方便,那么在实际的加法运算中
我们真实处理的数据应该是一个被反转的元组
- 按小数点对齐
即按小数点分割,然后用 PadStart 和 PadEnd 补 0
- 转元组后反转方便从左往右计算
单独计算小数位或整数位,减少复杂度,如果有进位,就在元组的最前面添加 \”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
文章为作者独立观点不代本网立场,未经允许不得转载。