当前期刊数: 250
解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 49~56 题。
精读 实现 Flip<T>
,将对象 T
中 Key 与 Value 对调:
1 2 3 Flip <{ a : "x" , b : "y" , c : "z" }>; Flip <{ a : 1 , b : 2 , c : 3 }>; Flip <{ a : false , b : true }>;
在 keyof
描述对象时可以通过 as
追加变形,所以这道题应该这样处理:
1 2 3 type Flip <T> = { [K in keyof T as T[K]]: K }
由于 Key 位置只能是 String or Number,所以 T[K]
描述 Key 会显示错误,我们需要限定 Value 的类型:
1 2 3 type Flip <T extends Record <string , string | number >> = { [K in keyof T as T[K]]: K }
但这个答案无法通过测试用例 Flip<{ pi: 3.14; bool: true }>
,原因是 true
不能作为 Key。只能用字符串 'true'
作为 Key,所以我们得强行把 Key 位置转化为字符串:
1 2 3 4 type Flip <T extends Record <string , string | number | boolean >> = { [K in keyof T as `${T[K]} ` ]: K }
用 TS 实现斐波那契数列计算:
1 2 type Result1 = Fibonacci <3 > type Result2 = Fibonacci <8 >
由于测试用例没有特别大的 Case,我们可以放心用递归实现。JS 版的斐波那契非常自然,但 TS 版我们只能用数组长度模拟计算,代码写起来自然会比较扭曲。
首先需要一个额外变量标记递归了多少次,递归到第 N 次结束:
1 2 3 type Fibonacci <T extends number , N = [1 ]> = N['length' ] extends T ? ( ) : Fibonacci <T, [...N, 1 ]>
上面代码每次执行都判断是否递归完成,否则继续递归并把计数器加一。我们还需要一个数组存储答案,一个数组存储上一个数:
1 2 3 4 5 6 7 8 9 type Fibonacci < T extends number , N extends number [] = [1 ], Prev extends number [] = [1 ], Cur extends number [] = [1 ] > = N['length' ] extends T ? Prev ['length' ] : Fibonacci <T, [...N, 1 ], Cur , [...Prev , ...Cur ]>
递归时拿 Cur
代替下次的 Prev
,用 [...Prev, ...Cur]
代替下次的 Cur
,也就是说,下次的 Cur
符合斐波那契定义。
实现 AllCombinations<S>
对字符串 S
全排列:
1 2 type AllCombinations _ABC = AllCombinations <'ABC' >
首先要把 ABC
字符串拆成一个个独立的联合类型,进行二次组合才可能完成全排列:
1 2 3 type StrToUnion <S> = S extends `${infer F} ${infer R} ` ? F | StrToUnion <R> : never
infer
描述字符串时,第一个指向第一个字母,第二个指向剩余字母;对剩余字符串递归可以将其逐一拆解为单个字符并用 |
连接:
将 StrToUnion<'ABC'>
的结果记为 U
,则利用对象转联合类型特征,可以制造出 ABC
在三个字母时的全排列:
1 { [K in U]: `${K} ${AllCombinations<never , Exclude<U, K>>} ` }[U]
然而只要在每次递归时巧妙的加上 '' |
就可以直接得到答案了:
1 2 3 type AllCombinations <S extends string , U extends string = StrToUnion <S>> = | '' | { [K in U]: `${K} ${AllCombinations<never , Exclude<U, K>>} ` }[U]
为什么这么神奇呢?这是因为每次递归时都会经历 ''
、'A'
、'AB'
、'ABC'
这样逐渐累加字符的过程,而每次都会遇到 '' |
使其自然形成了联合类型,比如遇到 'A'
时,会自然形成 'A'
这项联合类型,同时继续用 'A'
与 Exclude<'A' | 'B' | 'C', 'A'>
进行组合。
更精妙的是,第一次执行时的 ''
填补了全排列的第一个 Case。
最后注意到上面的结果产生了一个 Error:”Type instantiation is excessively deep and possibly infinite”,即这样递归可能产生死循环,因为 Exclude<U, K>
的结果可能是 never
,所以最后在开头修补一下对 never
的判否,利用之前学习的知识,never
不会进行联合类型展开,所以我们用 [never]
判断来规避:
1 2 3 4 5 6 type AllCombinations <S extends string , U extends string = StrToUnion <S>> = [ U ] extends [never ] ? '' : '' | { [K in U]: `${K} ${AllCombinations<never , Exclude<U, K>>} ` }[U]
实现 GreaterThan<T, U>
判断 T > U
:
1 2 3 4 GreaterThan <2 , 1 > GreaterThan <1 , 1 > GreaterThan <10 , 100 > GreaterThan <111 , 11 >
因为 TS 不支持加减法与大小判断,看到这道题时就应该想到有两种做法,一种是递归,但会受限于入参数量限制,可能堆栈溢出,一种是参考 MinusOne 的特殊方法,用巧妙的方式构造出长度符合预期的数组,用数组 ['length']
进行比较。
先说第一种,递归肯定要有一个递增 Key,拿 T
U
先后进行对比,谁先追上这个数,谁就是较小的那个:
1 2 3 4 5 6 type GreaterThan <T, U, R extends number [] = []> = T extends R['length' ] ? false : U extends R['length' ] ? true : GreaterThan <T, U, [...R, 1 ]>
另一种做法是快速构造两个长度分别等于 T
U
的数组,用数组快速判断谁更长。构造方式不再展开,参考 MinusOne
那篇的方法即可,重点说下如何快速判断 [1, 1]
与 [1, 1, 1]
谁更大。
因为 TS 没有大小判断能力,所以拿到了 ['length']
也没有用,我们得考虑 arr1 extends arr2
这种方式。可惜的是,长度不相等的数组,extends
永远等于 false
:
1 2 3 [1 ,1 ,1 ,1 ] extends [1 ,1 ,1 ] ? true : false [1 ,1 ,1 ] extends [1 ,1 ,1 ,1 ] ? true : false [1 ,1 ,1 ] extends [1 ,1 ,1 ] ? true : false
但我们期望进行如下判断:
1 2 3 ArrGreaterThan <[1 ,1 ,1 ,1 ],[1 ,1 ,1 ]> ArrGreaterThan <[1 ,1 ,1 ],[1 ,1 ,1 ,1 ]> ArrGreaterThan <[1 ,1 ,1 ],[1 ,1 ,1 ]>
解决方法非常体现 TS 思维:既然俩数组相等才返回 true
,那我们用 [...T, ...any]
进行补充判定,如果能判定为 true
,就说明前者长度更短(因为后者补充几项后可以判等):
1 2 3 type ArrGreaterThan <T extends 1 [], U extends 1 []> = U extends [...T, ...any ] ? false : true
这样一来,第二种答案就是这样的:
1 2 3 4 5 type GreaterThan <T extends number , U extends number > = ArrGreaterThan < NumberToArr <T>, NumberToArr <U> >
实现 TS 版 Zip
函数:
1 type exp = Zip <[1 , 2 ], [true , false ]>
此题同样配合辅助变量,进行计数递归,并额外用一个类型变量存储结果:
1 2 3 4 5 6 7 8 9 10 11 type Zip < T extends any [], U extends any [], I extends number [] = [], R extends any [] = [] > = I['length' ] extends T['length' ] ? R : U[I['length' ]] extends undefined ? Zip <T, U, [...I, 0 ], R> : Zip <T, U, [...I, 0 ], [...R, [T[I['length' ]], U[I['length' ]]]]>
[...R, [T[I['length']], U[I['length']]]]
在每次递归时按照 Zip 规则添加一条结果,其中 I['length']
起到的作用类似 for 循环的下标 i,只是在 TS 语法中,我们只能用数组的方式模拟这种计数。
实现 IsTuple<T>
判断 T
是否为元组类型(Tuple):
1 2 3 type case1 = IsTuple <[number ]> type case2 = IsTuple <readonly [number ]> type case3 = IsTuple <number []>
不得不吐槽的是,无论是 TS 内部或者词法解析都是更有效的判断方式,但如果用 TS 来实现,就要换一种思路了。
Tuple 与 Array 在 TS 里的区别是前者长度有限,后者长度无限,从结果来看,如果访问其 ['length']
属性,前者一定是一个固定数字,而后者返回 number
,用这个特性判断即可:
1 2 3 4 5 6 7 8 type IsTuple <T> = [T] extends [never ] ? false : T extends readonly any [] ? number extends T['length' ] ? false : true : false
其实这个答案是根据单测一点点试出来的,因为存在 IsTuple<{ length: 1 }>
单测用例,它可以通过 number extends T['length']
的校验,但因为其本身不是数组类型,所以无法通过 T extends readonly any[]
的前置判断。
实现 TS 版 Chunk
:
1 2 3 type exp1 = Chunk <[1 , 2 , 3 ], 2 > type exp2 = Chunk <[1 , 2 , 3 ], 4 > type exp3 = Chunk <[1 , 2 , 3 ], 1 >
老办法还是要递归,需要一个变量记录当前收集到 Chunk 里的内容,在 Chunk 达到上限时释放出来,同时也要注意未达到上限就结束时也要释放出来。
1 2 3 4 5 6 7 8 9 type Chunk < T extends any [], N extends number = 1 , Chunked extends any [] = [] > = T extends [infer First , ...infer Last ] ? Chunked ['length' ] extends N ? [Chunked , ...Chunk <T, N>] : Chunk <Last , N, [...Chunked , First ]> : [Chunked ]
Chunked['length'] extends N
判断 Chunked
数组长度达到 N
后就释放出来,否则把当前数组第一项 First
继续塞到 Chunked
数组,数组项从 Last
开始继续递归。
我们发现 Chunk<[], 1>
这个单测没过,因为当 Chunked
没有项目时,就无需成组了,所以完整的答案是:
1 2 3 4 5 6 7 8 9 10 11 12 type Chunk < T extends any [], N extends number = 1 , Chunked extends any [] = [] > = T extends [infer Head , ...infer Tail ] ? Chunked ['length' ] extends N ? [Chunked , ...Chunk <T, N>] : Chunk <Tail , N, [...Chunked , Head ]> : Chunked extends [] ? Chunked : [Chunked ]
实现 Fill<T, N, Start?, End?>
,将数组 T
的每一项替换为 N
:
1 type exp = Fill <[1 , 2 , 3 ], 0 >
这道题也需要用递归 + Flag 方式解决,即定义一个 I
表示当前递归的下标,一个 Flag
表示是否到了要替换的下标,只要到了这个下标,该 Flag
就永远为 true
:
1 2 3 4 5 6 7 8 type Fill < T extends unknown [], N, Start extends number = 0 , End extends number = T['length' ], I extends any [] = [], Flag extends boolean = I['length' ] extends Start ? true : false >
由于递归会不断生成完整答案,我们将 T
定义为可变的,即每次仅处理第一条,如果当前 Flag
为 true
就采用替换值 N
,否则就拿原本的第一个字符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Fill < T extends unknown [], N, Start extends number = 0 , End extends number = T['length' ], I extends any [] = [], Flag extends boolean = I['length' ] extends Start ? true : false > = I['length' ] extends End ? T : T extends [infer F, ...infer R] ? Flag extends false ? [F, ...Fill <R, N, Start , End , [...I, 0 ]>] : [N, ...Fill <R, N, Start , End , [...I, 0 ]>] : T
但这个答案没有通过测试,仔细想想发现 Flag
在 I
长度超过 Start
后就判定失败了,为了让超过后维持 true
,在 Flag
为 true
时将其传入覆盖后续值即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type Fill < T extends unknown [], N, Start extends number = 0 , End extends number = T['length' ], I extends any [] = [], Flag extends boolean = I['length' ] extends Start ? true : false > = I['length' ] extends End ? T : T extends [infer F, ...infer R] ? Flag extends false ? [F, ...Fill <R, N, Start , End , [...I, 0 ]>] : [N, ...Fill <R, N, Start , End , [...I, 0 ], Flag >] : T
总结 勤用递归、辅助变量可以解决大部分本周遇到的问题。
讨论地址是:精读《Flip, Fibonacci, AllCombinations…》· Issue ##432 · dt-fe/weekly
如果你想参与讨论,请 点击这里 ,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证 )