解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 9~16 题。
精读
Promise.all
实现函数 PromiseAll
,输入 PromiseLike,输出 Promise
,其中 T
是输入的解析结果:
const promiseAllTest1 = PromiseAll([1, 2, 3] as const)
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])
该题难点不在
Promise
如何处理,而是在于 { [K in keyof T]: T[K] }
在 TS 同样适用于描述数组,这是 JS 选手无论如何也想不到的:// 本题答案
declare function PromiseAll(values: T): Promise<{
[K in keyof T]: T[K] extends Promise ? U : T[K]
}>
不知道是 bug 还是 feature,TS 的
{ [K in keyof T]: T[K] }
能同时兼容元组、数组与对象类型。Type Lookup
实现
LookUp
,从联合类型 T
中查找 type
为 P
的项并返回:interface Cat {
type: 'cat'
breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}interface Dog {
type: 'dog'
breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
color: 'brown' | 'white' | 'black'
}type MyDog = LookUp // expected to be `Dog`
该题比较简单,只要学会灵活使用
infer
与 extends
即可:// 本题答案
type LookUp = T extends {
type: infer U
} ? (
U extends P ? T : never
) : never
联合类型的判断是一个个来的,所以我们只要针对每一个单独写判断就行了。上面的解法中,我们先利用
extend
+ infer
锁定 T
的类型是包含 type
key 的对象,且将 infer U
指向了 type
,所以在内部再利用三元运算符判断 U extends P ?
就能将 type
命中的类型挑出来。笔者翻了下答案,发现还有一种更高级的解法:
// 本题答案
type LookUp = U extends { type: T } ? U : never
该解法更简洁,更完备:
- 在泛型处利用
extends { type: any }
、extends U['type']
直接锁定入参类型,让错误校验更早发生。 T extends U['type']
精确缩小了参数T
范围,可以学到的是,之前定义的泛型U
可以直接被后面的新泛型使用。U extends { type: T }
是一种新的思考角度。在第一个答案中,我们的思维方式是 “找到对象中type
值进行判断”,而第二个答案直接用整个对象结构{ type: T }
判断,是更纯粹的 TS 思维。
实现
TrimLeft
,将字符串左侧空格清空:type trimed = TrimLeft<'Hello World'> // expected to be 'Hello World'
在 TS 处理这类问题只能用递归,不能用正则。比较容易想到的是下面的写法:
// 本题答案
type TrimLeft = T extends ` ${infer R}` ? TrimLeft : T
即如果字符串前面包含空格,就把空格去了继续递归,否则返回字符串本身。掌握该题的关键是
infer
也能用在字符串内进行推导。Trim
实现
Trim
,将字符串左右两侧空格清空:type trimmed = Trim<'Hello World'> // expected to be 'Hello World'
这个问题简单的解法是,左右都 Trim 一下:
// 本题答案
type Trim = TrimLeft>
type TrimLeft = T extends ` ${infer R}` ? TrimLeft : T
type TrimRight = T extends `${infer R} ` ? TrimRight : T
这个成本很低,性能也不差,因为单写
TrimLeft
与 TrimRight
都很简单。如果不采用先 Left 后 Right 的做法,想要一次性完成,就要有一些 TS 思维了。比较笨的思路是 “如果左边有空格就切分左边,或者右边有空格就切分右边”,最后写出来一个复杂的三元表达式。比较优秀的思路是利用 TS 联合类型:
// 本题答案
type Trim =T extends ` ${infer R}` | `${infer R} ` ? Trim : T
extends
后面还可以跟联合类型,这样任意一个匹配都会走到 Trim
递归里。这就是比较难说清楚的 TS 思维,如果没有它,你只能想到三元表达式,但一旦理解了联合类型还可以在 extends
里这么用,TS 帮你做了 N 元表达式的能力,那么写出来的代码就会非常清秀。Capitalize
实现
Capitalize
将字符串第一个字母大写:type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'
如果这是一道 JS 题那就简单到爆,可题目是 TS 的,我们需要再度切换为 TS 思维。
首先要知道利用基础函数
Uppercase
将单个字母转化为大写,然后配合 infer
就不用多说了:type MyCapitalize = T extends `${infer F}${infer U}` ? `${Uppercase}${U}` : T
Replace
实现 TS 版函数
Replace
,将字符串 From
替换为 To
:type replaced = Replace<'types are fun!', 'fun', 'awesome'> // expected to be 'types are awesome!'
把
From
夹在字符串中间,前后用两个 infer
推导,最后输出时前后不变,把 From
换成 To
就行了:// 本题答案
type Replace =
S extends `${infer A}${From}${infer B}` ? `${A}${To}${B}` : S
ReplaceAll
实现
ReplaceAll
,将字符串 From
替换为 To
:type replaced = ReplaceAll<'t y p e s', ' ', ''> // expected to be 'types'
该题与上题不同之处在于替换全部,解法肯定是递归,关键是何时递归的判断条件是什么。经过一番思考,如果
infer From
能匹配到不就说明还可以递归吗?所以加一层三元判断 From extends ''
即可:// 本题答案
type ReplaceAll =
S extends `${infer A}${From}${infer B}` ? (
From extends '' ? `${A}${To}${B}` : ReplaceAll<`${A}${To}${B}`, From, To>
) : S
Append Argument
实现类型
AppendArgument
,将函数参数拓展一个:type Fn = (a: number, b: string) => numbertype Result = AppendArgument
// expected be (a: number, b: string, x: boolean) => number
该题很简单,用
infer
就行了:// 本题答案
type AppendArgument = F extends (...args: infer T) => infer R ? (...args: [...T, E]) => R : F
总结 这几道题都比较简单,主要考察对
infer
和递归的熟练使用。讨论地址是:精读《Promise.all, Replace, Type Lookup...》· Issue #425 · dt-fe/weekly如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号【精读《Promise.all|精读《Promise.all, Replace, Type Lookup...》】
文章图片
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)
推荐阅读
- Python字符串方法|设置S3(strip,lstrip,rstrip,min,max,maketrans,translate,replace和expandtabs)
- Pandas DataFrame.replace()用法