解决TS题最好的方法就是多练习。这次的interpretingtype-challengesMedium的难度是33~40题。MinusOne精读使用TS实现MinusOne对一个数进行减一:typeZero=MinusOne<1>//0typeFiftyFour=MinusOne<55>//54TS没有“普通”的计算能力,但是有出路当涉及到数字时,也就是TS可以通过['length']来访问数组长度,几乎所有的数值计算都是从它推导出来的。对于这道题,我们只需要构造一个长度为泛型length-1的数组,获取它的['length']属性即可,但是这个解法有个缺陷,不能计算负值,因为数组的长度不能小于0://这个问题的答案类型MinusOne=[...arr,'']['length']extendsT?arr['length']:MinusOne这个方案的原理不是原来的数-1,而是从0开始连续加1,直到目标数减一。但是这个解决方案没有通过MinusOne<1101>测试,因为1000次递归次数是上限。还有另一个想法可以破坏递归,即:typeCount=['1','1','1']extends[...inferT,'1']?T['length']:0//2是将负一转化为extends[...inferT,'1'],这样数组T的长度正好等于答案。那么难点就变成了如何根据传入的数字构造一个等长的数组呢?即问题就变成了如何实现CountTo生成长度为N且每一项为1的数组,而且生成的数组递归效率必须高,否则会遇到递归上限的问题.网上有个神仙解决办法,我自己也想不出来,不过可以拿出来给大家分析一下:typeCountTo=Textends`${首先推断}${推断其余}`?CountTo[keyofN&First]>:CounttypeN={'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,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]}即该方法可以高效实现CountTo<'1000'>生成一个长度为1000,每一项为1的数组,具体来说,只需要遍历字符串length次,比如1000只需要递归4次,10000只需要递归5次。CountTo函数体的逻辑是,如果字符串T不为空,则将其拆分为第一个字符First和其余字符Rest,然后递归取其余字符,但一次生成First到正确的长度.核心逻辑是函数N,实际上是将T的数组长度扩大10倍,并在数组末尾加上当前1的个数。而N&First的key也是神来之笔。这里的本意是访问First下标,但是TS并不知道它是一个安全可访问的下标,而N&First的keyof最终的值仍然是First,也可以被TS安全识别为下标。以CountTo<'123'>为例:First='1',Rest='23'第一次执行:CountTo<'23',N<[]>['1']>//展开时,...[]还是[],所以最后的结果是['1']第二次执行First='2',Rest='3'CountTo<'3',N<['1']>['2']>//展开时,...[]有10个,所以['1']变成10个1,在N映射表中增加2个1,现在一共有12个1,第三次执行First='3',Rest=''CountTo<'',N<['1',...12intotal]>['3']>//展开后,...[]有10,所以12个A1变成120,加上映射表中的3,一共有123个1总结一下,就是把数字T变成一个字符串,从最左边开始,每次把数组的累加个数乘以10添加1到当前值的个数大大减少了递归的次数。PickByType实现了PickByType,并在对象P中保留了类型Q的键:启用:布尔值;}这道题很简单,因为我们在遇到RemoveIndexSignature题时,用KinkeyofPasxxx进一步判断Key位置,所以只要P[K]extendsQ,就保留,否则returnnever。是的://这个问题的答案类型PickByType
={[KinkeyofPasP[K]extendsQ?K:never]:P[K]}StartsWithimplementsStartsWith判断字符串T是否以U开头:typea=StartsWith<'abc','ac'>//expectedtobefalsetypeb=StartsWith<'abc','ab'>//expectedtobetruetypec=StartsWith<'abc','abcd'>//expectedtobefalse这道题比较简单,可以通过递归+先解决字符判断://本题答案类型StartsWith=Uextends`${inferUS}${inferUE}`?T扩展了`${inferTS}${inferTE}`?TS扩展美国?StartsWith:false:false:true思路是:U如果是空串场景就匹配所有,直接返回真的;否则,可以将U拆分为US(UStart)和UE(UEnd)开头的字符串进行后续判断,再进行上述判断。如果T为空字符串,则无法被U匹配到,直接返回false;否则,T可以拆分为以TS(TStart)和TE(TEnd)开头的字符串,以供后续判断。按照上面的判断,如果TSextendsUS表示本次第一个字符匹配,则递归匹配剩余的字符StartsWith,如果第一个字符不匹配则提前返回false。看了一些答案,作者发现还有一个降维攻击方案://本题答案类型StartsWith=Textends`${U}${string}`?true:false可以用${string}匹配任意字符串进行extends判断,有点正则。当然${string}也可以换成${inferX},但是得到的X就不用再用了://本题答案typeStartsWith=T扩展`${U}${inferX}`?true:false作者也尝试了下面的答案,当后缀Diff部分是stringlikenumber时也是正确的://本题答案typeStartsWith=Textends`${U}${数字}`?true:false表示字符串模板最常用的引用是${inferX}或${string},如果要匹配特定的数字字符串,也可以使用${number}。EndsWith实现EndsWith来判断字符串T是否以U结尾:typea=EndsWith<'abc','bc'>//expectedtobetruetypeb=EndsWith<'abc','abc'>//expectedtobetruetypec=EndsWith<'abc','d'>//expectedtofalse有了上一题的经验,这道题应该不会太简单://这道题的答案typeEndsWith=Textends`${string}${U}`?true:false可以看出,TS的技巧掌握了就很简单,但是如果不会,几乎无解,或者用笨递归来解决。PartialByKeys实现了PartialByKeys,使与K匹配的Key成为可选定义。如果不传K,效果同Partial:interfaceUser{name:stringage:numberaddress:string}typeUserPartialName=PartialByKeys//{name?:string;年龄:数字;address:string}看到题目要求Partial的行为在没有传递参数时是相同的,你应该能想到你应该开始写一个这样的默认值:typePartialByKeys={}我们要用optional和non-optional来共同描述两个对象,因为TS不支持同一个对象有两个keyof描述,所以只能写成两个Object:typePartialByKeys={[QinkeyofTasQextendsK?Q:never]?:T[Q]}&{[QinkeyofTasQextendsK?never:Q]:T[Q]}但是没有匹配到测试用例,原因是最终的类型是正确的,但是因为分为两个对象,不能匹配到一个对象中,所以需要合并有点神奇的行为://这个问题的答案类型PartialByKeys={[QinkeyofTasQextendsK?Q:never]?:T[Q]}&{[QinkeyofTasQextendsK?从不:Q]:T[Q]}extendsinferR?{[QinkeyofR]:R[Q]}:never再次扩展一个对象extendsinferR看似毫无意义,但确实让类型合并到一个对象中,这很有趣。我们也可以将其提取到一个函数Merge中使用。这道题还有一个函数组合答案://TheanswertothisquestiontypeMerge={[KinkeyofT]:T[K]}typePartialByKeys=Merge&Omit>使用Partial&Omit合并对象。因为Omit中的K有来自keyofT的限制,而测试用例包含一个不存在的Key值,如unknown,你可以使用extendsPropertyKey来处理这种情况。RequiredByKeys实现RequiredByKeys,使Key匹配K成为强制定义。如果不传K,效果同Required:interfaceUser{name?:stringage?:numberaddress?:string}typeUserRequiredName=RequiredByKeys//{name:string;年龄?:数字;address?:string}正好和上面的问题相反,答案已经准备好了:typeMerge={[KinkeyofT]:T[K]}typeRequiredByKeys=Merge&Omit>等等,没有测试用例通过,为什么?仔细想想,发现确实暗藏玄机:Merge<{a:number}&{a?:number}>//结果为{a:number},即当同一个Key可选且mandatory,合并结果为mandatoryselect。上一题因为Omit是必选项,optional不会被必选项覆盖,但是这道题Merge&Omit>,前面的Required肯定有最高优先级。下面的Omit虽然逻辑上是正确的,但是不能把强制选择覆盖为optional,所以测试用例都挂了。解决办法是打破这个特性,使用原来的对象&只包含K的强制对象,让强制覆盖之前的可选Key。后者可以挑出来:typeMerge={[KinkeyofT]:T[K]}typeRequiredByKeys=Merge>>这样,只有一个测试失败:Expect,UserRequiredName>>我们也需要兼容Pick来访问不存在的Key,使用extends来避免://AnswertothisquestiontypeMerge={[KinkeyofT]:T[K]}typeRequiredByKeys=Merge>>MutableimplementsMutable,使对象T的所有键都可写:interfaceTodo{readonlytitle:stringreadonlydescription:stringreadonlycompleted:boolean}typeMutableTodo=Mutable//{title:string;描述:字符串;完成:布尔值;}把对象从不可写变成可写:typeReadonly={readonly[KinkeyofT]:T[K]}从可写变成不可写也好写,主要看你记不记得thisgrammar:-readonly://这个问题的答案typeMutable={-readonly[KinkeyofT]:T[K]}OmitByType实现OmitByType根据类型U排除T中的Keys:count:number}这道题和PickByType正好相反,只是把extends后面的内容交换一下://TheanswertothisquestiontypeOmitByType={[KinkeyofTasT[K]extendsU?never:K]:T[K]}总结这周的题目除了MinusOne的神仙解法比较难以外其他都比较普通。其中,Merge函数的神奇用途需要了解一下。讨论地址为:精读《MinusOne, PickByType, StartsWith...》·Issue#430·dt-fe/weekly想加入讨论点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)