当前位置: 首页 > 科技观察

一个真实的案例说明了TypeScript类型体操的意义

时间:2023-03-13 12:00:03 科技观察

TypeScript类型系统支持类型编程,即对类型参数进行一系列操作以生成新的类型。例如:类型isTwo=Textends2?真假;这种编程逻辑可以写得很复杂,所以被戏称为“类型体操”。是TS最强大复杂的部分,属于深水区。很多同学不知道学习类型编程有什么用,好像业务不需要一样。那么今天我们就通过一个具体的例子来感受一下体操的意义。我们要实现这样一个JS方法:functionparseQueryString(queryStr){if(!queryStr||!queryStr.length){return{};}constqueryObj={};constitems=queryStr.split('&');items.forEach(item=>{const[key,value]=item.split('=');if(queryObj[key]){if(Array.isArray(queryObj[key])){queryObj[key].推送(值);}else{queryObj[key]=[queryObj[key],value]}}else{queryObj[key]=value;}});returnqueryObj;}这段代码很容易看出是在做querystring的解析,将'a=1&b=2&c=3'的字符串解析成{a:1,b:2,c:3}返回.如果有同名的键,则将其合并到一个数组中。你写了很多JS的逻辑,这部分很容易理解:如果你想给这个函数加一个类型,你会怎么加?我猜,大部分人会这样加上:参数是string类型,返回值是parse下面的object类型object。这是可以的,object也可以写成Record,因为object是一个索引类型(索引类型就是聚合了多个元素的类型,比如对象,类,数组)。Record是TS内置的高级类型,通过映射类型的语法生成索引类型:typeRecord={[PinK]:T;}比如传入'a'|'b'作为key,1作为value,就可以生成这样的索引类型:所以这里的Record表示key是string类型,value是任意类型的索引类型,可以用object代替,比较语义化:但是返回值类型是object还是Recordexists一个问题:返回的object不能提示它有什么属性:对于习惯了ts提示的同学来说,没有提示太难受了。如何提示此函数的返回类型?这需要类型编程。我们把函数的类型定义改成这样:声明一个类型参数Str,它被约束为字符串类型,函数参数的类型指定为这个Str,返回值的类型通过执行type得到对Str的操作,即ParseQueryString。这个ParseQueryString类型所做的就是通过对传入的Str进行各种类型的操作,生成对应的索引类型。这样,返回的类型就有了提示:是不是很神奇!这就是体操的魅力!可以实现更精确的类型提示。那么高级类型的ParseQueryString是如何实现的呢?事实上,我们已经实现了:TSTypeGymnastics:复杂高级类型的图表。这里再说一下:首先,我们需要将'a=1&b=2&c=3'字符串用&分隔,使用模式匹配。分别对提取出来的a=1、b=2、c=3等字符串分别进行处理,合并结果返回。那就是:typeParseQueryString=Strextends`${inferParam}&${inferRest}`?MergeParams,ParseQueryString>:ParseParam;类型参数Str是要处理的字符串类型,通过模式匹配将&分隔的字符串提取到infer声明的局部变量Param中,其余放入infer声明的局部变量Rest中。然后对提取出来的Param进行处理,即ParseParam,其余递归处理,即ParseQueryString,再将结果进行组合。如果模式匹配不满足,说明没有&,然后处理剩下的返回。这里的ParseParam处理的是a=1,b=2,c=3这样的字符串,也是通过模式匹配来提取的:typeParseParam=Paramextends`${inferKey}=${inferValue}`?{[KinKey]:Value}:Record;类型参数Param为待处理的字符串,由=分隔的字符串通过模式匹配提取到局部变量Key和Value中,构造为索引类型返回;如果不满足模式匹配,则表示不是=号分隔的字符串字面量类型,返回Record表示任意索引类型。测试下:然后对于多个索引类型的合并,通过映射类型的语法构造一个新的索引类型:typeMergeParams,OtherParamextendsRecord>={只读[输入keyofOneParam|keyofOtherParam]:KeyextendskeyofOneParam?Key扩展了keyofOtherParam?MergeValues:OneParam[Key]:KeyextendskeyofOtherParam?OtherParam[Key]:never}类型参数OneParam和OtherParam是两种索引类型,受Record约束。通过映射类型的语法构造一个新的索引类型return,Key来自于两者的结合,即keyofOneParam|中的Key其他参数的键。并添加一个readonly修饰,使得返回的索引类型不可修改。应该判断价值。如果是两者的值,则合并,否则分别取对应的值。合并的逻辑如下:typeMergeValues=OneextendsOther?一:Otherextendsunknown[]?[一个,...其他]:[一个,其他];类型参数One和Other是要合并的两个值的类型,如果两者相同则返回其中一个,否则如果是数组则合并数组,即[One,...Other],否则将两个值合并成一个数组[One,Other]。这样就完成了两种索引类型的合并。测试一下:总体测试下:成功!我们已经实现了ParseQueryString的高级类型!(不懂体操的可以看看我的小册子《TypeScript 类型体操通关秘籍》补底)。当然,这只是纯类型体操,最终目的是为了在JS中使用,所以我们将parseQueryString的类型定义改成这样:将函数参数的类型传入ParseQueryString的高级类型进行类型计算,以及返回结果函数返回值的类型。(这里需要使用asany来断言返回值为any,因为默认推导的类型不准确,所以我们使用根据str动态计算的类型)这样可以实现精确的类型提示:而且由于我们的readonly局限性,不能修改属性的值:与无用的类型体操相比:可以得出结论,类型编程可以通过类型操作生成更准确的类型,并配合编辑器做更准确的类型提示和检查。这就是体操的意义所在。总结类型编程是TypeScript的深水内容。它在对类型执行一系列类型操作后生成新类型。它可以实现更精确的类型提示和检查。通过parseQueryString函数的类型定义,我们可以直观的体验类型操和非类型操的区别。在类型提示方面,体验是截然不同的。实现更精确的类型提示和检查,这就是类型体操的意义所在!