当前期刊数: 248
解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 33~40 题。
精读
MinusOne
用 TS 实现 MinusOne
将一个数字减一:
1 |
|
TS 没有 “普通” 的运算能力,但涉及数字却有一条生路,即 TS 可通过 ['length']
访问数组长度,几乎所有数字计算都是通过它推导出来的。
这道题,我们只要构造一个长度为泛型长度 -1 的数组,获取其 ['length']
属性即可,但该方案有一个硬伤,无法计算负值,因为数组长度不可能小于 0:
1 |
|
该方案的原理不是原数字 -1,而是从 0 开始不断加 1,一直加到目标数字减一。但该方案没有通过 MinusOne<1101>
测试,因为递归 1000 次就是上限了。
还有一种能打破递归的思路,即:
1 |
|
也就是把减一转化为 extends [...infer T, '1']
,这样数组 T
的长度刚好等于答案。那么难点就变成了如何根据传入的数字构造一个等长的数组?即问题变成了如何实现 CountTo<N>
生成一个长度为 N
,每项均为 1
的数组,而且生成数组的递归效率也要高,否则还会遇到递归上限的问题。
网上有一个神仙解法,笔者自己想不到,但是可以拿出来给大家分析下:
1 |
|
也就是该方法可以高效的实现 CountTo<'1000'>
产生长度为 1000,每项为 1
的数组,更具体一点,只需要遍历 <T>
字符串长度次数,比如 1000
只要递归 4 次,而 10000
也只需要递归 5 次。
CountTo
函数体的逻辑是,如果字符串 T
非空,就拆为第一个字符 First
与剩余字符 Rest
,然后拿剩余字符递归,但是把 First
一次性生成到了正确的长度。最核心的逻辑就是函数 N<T>
了,它做的其实是把 T
的数组长度放大 10 倍再追加上当前数量的 1 在数组末尾。
而 keyof N & First
也是神来之笔,此处本意就是访问 First
下标,但 TS 不知道它是一个安全可访问的下标,而 keyof N & First
最终值还是 First
,也可以被 TS 安全识别为下标。
拿 CountTo<'123'>
举例:
第一次执行 First='1'
、Rest='23'
:
1 |
|
第二次执行 First='2'
、Rest='3'
1 |
|
第三次执行 First='3'
、Rest=''
1 |
|
总结一下,就是将数字 T
变成字符串,从最左侧开始获取,每次都把已经积累的数组数量乘以 10 再追加上当前值数量的 1,实现递归次数极大降低。
PickByType
实现 PickByType<P, Q>
,将对象 P
中类型为 Q
的 key 保留:
1 |
|
本题很简单,因为之前碰到 Remove Index Signature 题目时,我们用了 K in keyof P as xxx
来对 Key 位置进行进一步判断,所以只要 P[K] extends Q
就保留,否则返回 never
即可:
1 |
|
StartsWith
实现 StartsWith<T, U>
判断字符串 T
是否以 U
开头:
1 |
|
本题也比较简单,用递归 + 首字符判等即可破解:
1 |
|
思路是:
U
如果为空字符串则匹配一切场景,直接返回true
;否则U
可以拆为以US
(U Start) 开头、UE
(U End) 的字符串进行后续判定。- 接着上面的判定,如果
T
为空字符串则不可能被U
匹配,直接返回false
;否则T
可以拆为以TS
(T Start) 开头、TE
(T End) 的字符串进行后续判定。 - 接着上面的判定,如果
TS extends US
说明此次首字符匹配了,则递归匹配剩余字符StartsWith<TE, UE>
,如果首字符不匹配提前返回false
。
笔者看了一些答案后发现还有一种降维打击方案:
1 |
|
没想到还可以用 ${string}
匹配任意字符串进行 extends
判定,有点正则的意思了。当然 ${string}
也可以被 ${infer X}
代替,只是拿到的 X
不需要再用到了:
1 |
|
笔者还试了下面的答案在后缀 Diff 部分为 string like number 时也正确:
1 |
|
说明字符串模板最通用的指代是 ${infer X}
或 ${string}
,如果要匹配特定的数字类字符串也可以混用 ${number}
。
EndsWith
实现 EndsWith<T, U>
判断字符串 T
是否以 U
结尾:
1 |
|
有了上题的经验,这道题不要太简单:
1 |
|
这可以看出 TS 的技巧掌握了就非常简单,但不知道就几乎无解,或者用很笨的递归来解决。
PartialByKeys
实现 PartialByKeys<T, K>
,使 K
匹配的 Key 变成可选的定义,如果不传 K
效果与 Partial<T>
一样:
1 |
|
看到题目要求是不传参数时和 Partial<T>
行为一直,就应该能想到应该这么起头写个默认值:
1 |
|
我们得用可选与不可选分别描述两个对象拼起来,因为 TS 不支持同一个对象下用两个 keyof
描述,所以只能写成两个对象:
1 |
|
但不匹配测试用例,原因是最终类型正确,但因为分成了两个对象合并无法匹配成一个对象,所以需要用一点点 Magic 行为合并:
1 |
|
将一个对象 extends infer R
再重新展开一遍看似无意义,但确实让类型上合并成了一个对象,很有意思。我们也可以将其抽成一个函数 Merge<T>
来使用。
本题还有一个函数组合的答案:
1 |
|
- 利用
Partial & Omit
来合并对象。 - 因为
Omit<T, K>
中K
有来自于keyof T
的限制,而测试用例又包含unknown
这种不存在的 Key 值,此时可以用extends PropertyKey
处理此场景。
RequiredByKeys
实现 RequiredByKeys<T, K>
,使 K
匹配的 Key 变成必选的定义,如果不传 K
效果与 Required<T>
一样:
1 |
|
和上题正好相反,答案也呼之欲出了:
1 |
|
等等,一个测试用例都没过,为啥呢?仔细想想发现确实暗藏玄机:
1 |
|
也就是同一个 Key 可选与必选同时存在时,合并结果是必选。上一题因为将必选 Omit
掉了,所以可选不会被必选覆盖,但本题 Merge<Required<T> & Omit<T, K>>
,前面的 Required<T>
必选优先级最高,后面的 Omit<T, K>
虽然本身逻辑没错,但无法把必选覆盖为可选,因此测试用例都挂了。
解法就是破解这一特征,用原始对象 & 仅包含 K
的必选对象,使必选覆盖前面的可选 Key。后者可以 Pick
出来:
1 |
|
这样就剩一个单测没通过了:
1 |
|
我们还要兼容 Pick
访问不存在的 Key,用 extends
躲避一下即可:
1 |
|
Mutable
实现 Mutable<T>
,将对象 T
的所有 Key 变得可写:
1 |
|
把对象从可写变成不可写:
1 |
|
从不可写改成可写也简单,主要看你是否记住了这个语法:-readonly
:
1 |
|
OmitByType
实现 OmitByType<T, U>
根据类型 U 排除 T 中的 Key:
1 |
|
本题和 PickByType
正好反过来,只要把 extends
后内容对调一下即可:
1 |
|
总结
本周的题目除了 MinusOne
那道神仙解法比较难以外,其他的都比较常见,其中 Merge
函数的妙用需要领悟一下。
讨论地址是:精读《MinusOne, PickByType, StartsWith…》· Issue ##430 · dt-fe/weekly
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)