解决TS题最好的方法就是多练习。这次我解读type-challenges中等难度1~8题。精读GetReturnType实现了一个很经典的ReturnType:constfn=(v:boolean)=>{if(v)return1elsereturn2}typea=MyReturnType//shouldbe"1|2“首先,不要被例子吓倒。您必须执行代码才能知道返回类型。其实TS已经帮我们推导出了返回类型,所以上面函数fn的类型已经是这样了:constfn=(v:boolean):1|2=>{...}我们要做的是从内部提取函数的返回值,这很适合infer实现://本题的答案typeMyReturnType=Textends(...args:any[])=>推断P?P:neverinferwithextends是解构复杂类型的神器。如果你不能一眼看懂上面的代码,说明你对infer不够熟悉,需要多看书。Omit实现了Omit,与Pick正好相反,排除了对象T中的K键:interfaceTodo{title:stringdescription:stringcompleted:boolean}typeTodoPreview=MyOmit<待办事项,'描述'|'title'>consttodo:TodoPreview={completed:false,}这个问题更简单的解决方案是:typeMyOmit={[PinkeyofT]:PextendsK?never:T[P]}其实还是包含了description和title这两个key,但是这两个key的类型是never,不符合要求。所以只要把keyofT中的P写出来,不管后面怎么写,这个Key是抹不掉的。我们应该从Key开始:typeMyOmit={[Pin(keyofTextendsK?never:keyofT)]:T[P]}但是这样写还是不对。我们的思路是正确的,就是把属于K的keyofT排除掉,但是因为T的前后key是不相关的,所以需要用Exclude告诉TS,T的前后key是同一个reference(Exclude在上一讲实现了)://本题答案typeMyOmit={[PinExclude]:T[P]}typeExclude=T扩展U?从不:T这是正确的。掌握这道题的核心是:三元判断也可以写在Key位置。JS抽取函数和不抽取函数效果是一样的,只是TS需要推断。很多时候,提取一个函数就是告诉TS“是同一个引用”。当然,既然使用了Exclude,我们不妨结合Pick写一个更优雅的Omit实现://本题的优雅答案typeMyOmit=Pick>Readonly2实现MyReadonly2,使指定的KeyK成为ReadOnly:interfaceTodo{title:stringdescription:stringcompleted:boolean}consttodo:MyReadonly2={title:"Hey",description:"foobar",completed:false,}todo.title="Hello"//错误:不能重新分配只读属性todo.description="barFoo"//错误:不能reassignareadonlypropertytodo.completed=true//OK这道题乍一看很难,因为readonly必须定义在Key位置,但是我们不能在这个位置做三元判断。其实用我们之前做的Pick、Omit和内置Readonly的组合,就出来了://TheanswertothisquestiontypeMyReadonly2=Readonly>&Omit也就是我们可以把对象一分为二,先挑出K个Key部分设置为Readonly,然后用&合并剩下的Keys,正好用到函数中的Omit上一个问题,这是完美的。DeepReadonly为所有子元素递归地实现DeepReadonly:typeX={x:{a:1b:'hi'}y:'hey'}typeExpected={readonlyx:{readonlya:1readonlyb:'hi'}readonlyy:'hey'}typeTodo=DeepReadonly//应与Expected相同这必须按类型递归实现。由于需要递归,我们不能依赖内置的Readonly函数。我们需要将函数转换为Expandhandwriting://TheanswertothisquestiontypeDeepReadonly={readonly[KinkeyofT]:T[K]extendsObject>?DeepReadonly:T[K]}这里的对象也可以使用Record代替。TupletoUnion实现TupleToUnion并返回元组所有值的集合:typeArr=['1','2','3']typeTest=TupleToUnion//应该是'1'|'2'|'3'本题将元组类型转换为所有值的可能集合,即我们要使用所有下标来访问这个数组,只要在TS中使用[number]作为下标即可://本题的答案类型TupleToUnion=T[number]ChainableOptions直接看例子更容易理解:declareconstconfig:Chainableconstresult=config.option('foo',123).option('name','type-challenges').option('bar',{value:'HelloWorld'}).get()//期望结果类型为:interfaceResult{foo:numbername:stringbar:{value:string}}也就是我们实现了一个比较复杂的Chainable类型。这种类型的对象可以链式调用.option(key,value),直到使用get()得到聚合了所有options的对象。如果我们用JS实现这个功能,就必须把Object的值存到当前闭包中,然后提供get直接返回,或者option递归传入一个新值。我们不妨用Class来实现:classChain{constructor(previous={}){this.obj={...previous}}obj:Objectget(){returnthis.obj}option(key:string,value:any){returnnewChain({...this.obj,[key]:value})}}constconfig=newChain()并且本地要求用TS实现,比较有意思,对比一下JS和TS思维。我先说明一点。上面JS方法写完题之后,类型其实就出来了,但是用TS来完整实现类型还有其他的用处,尤其是在一些复杂的功能场景下,需要用TS系统来描述类型.JS真正实现的时候,获取任意类型进行纯运行时处理,类型和运行时分离。好,我们回到主题。我们先把Chainable的框架写出来:typeChainable={option:(key:string,value:any)=>anyget:()=>any}问题是,怎么用type来描述option之后,你能不能还收到选项或得到?更麻烦的是,如何一步步把type传过去,让get知道我这时候拿的是什么type?Chainable必须接收一个泛型类型,而这个泛型类型的默认值是一个空对象,所以config.get()返回一个空对象是合理的:typeChainable={option:(key:string,value:any)=>anyget:()=>Result}上面的代码对于第一层是完全没问题的,直接调用get返回一个空对象。第二步是解决递归问题://TheanswertothisquestiontypeChainable={option:(key:K,value:V)=>Chainableget:()=>Result}递归思维大家都懂,就不细说了。这里有一个看似不值一提的地方,但确实很容易骗人,就是如何描述一个对象只包含一个Key值,而这个值是一个泛型的K?//这是错误的,因为它描述了很多类型{[K]:V}//这也是错误的,这个K是字面量K,不是你要引用的类型{K:V},所以它必须使用TS“习惯法”中[KinkeyofT]的套路描述,即使我们知道T只有一种固定类型。可见JS和TS完全是两种思维方式,所以精通JS并不一定代表精通TS,TS还是需要多做题来培养思维。LastofArray实现Last以获取元组中最后一项的类型:typearr1=['a','b','c']typearr2=[3,2,1]typetail1=Last//expectedtobe'c'typetail2=Last//expectedtobe1我们之前已经实现了First,类似的,这里无非就是在解构的时候把最后一个描述为infer://答案对于这个问题类型Last=Textends[...inferQ,inferP]?P:never这里要注意inferQ有人可能是第一次写:typeLast=Textends[...Others,inferP]?P:永远不会发现错误,因为在TS中不可能使用一个未定义的泛型,如果你把Others放在Last中,你在TS中会面临一个大问题:typeLast=Textends[...Others,inferP]?P:never//一定要报错Last因为Last只传入了一个参数,所以一定要报错,但是第一个参数是用户对的,第二个参数是我们推导的。这里不能用默认值,必须写。没有解决办法。如果真要这样写,就必须用到一个TS还没有通过的特性:部分类型参数推断。例如,未来的语法很可能是:typeLast=Textends[...Others,inferP]?P:never这种方式,第一遍只需要一个参数,同时也声明了第二个参数是推断类型。不过这个提议目前还没有得到支持,从意义和效果上来说,本质上和写inferintoanexpression是一样的,所以不用担心这个问题。Pop实现Pop并在删除元组的最后一项后返回类型:typearr1=['a','b','c','d']typearr2=[3,2,1]typere1=Pop//expectedtobe['a','b','c']typere2=Pop//expectedtobe[3,2]这道题几乎和上一题一模一样,return第一个解构值就可以了://这个问题的答案typePop=Textends[...inferQ,inferP]?Q:从来没有有很大的区别。如果你想真正掌握TS,你必须做很多题。讨论地址是:Jingdu《Get return type, Omit, ReadOnly...》·Issue#422·dt-fe/weekly想参与讨论的请戳这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)