forked from ascoders/weekly
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
201 additions
and
1 deletion.
There are no files selected for viewing
199 changes: 199 additions & 0 deletions
199
TS 类型体操/245.精读《Promise.all, Replace, Type Lookup...》.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 9~16 题。 | ||
|
||
## 精读 | ||
|
||
### [Promise.all](https://github.com/type-challenges/type-challenges/blob/main/questions/00020-medium-promise-all/README.md) | ||
|
||
实现函数 `PromiseAll`,输入 PromiseLike,输出 `Promise<T>`,其中 `T` 是输入的解析结果: | ||
|
||
```ts | ||
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 选手无论如何也想不到的: | ||
|
||
```ts | ||
// 本题答案 | ||
declare function PromiseAll<T>(values: T): Promise<{ | ||
[K in keyof T]: T[K] extends Promise<infer U> ? U : T[K] | ||
}> | ||
``` | ||
|
||
不知道是 bug 还是 feature,TS 的 `{ [K in keyof T]: T[K] }` 能同时兼容元组、数组与对象类型。 | ||
|
||
### [Type Lookup](https://github.com/type-challenges/type-challenges/blob/main/questions/00062-medium-type-lookup/README.md) | ||
|
||
实现 `LookUp<T, P>`,从联合类型 `T` 中查找 `type` 为 `P` 的项并返回: | ||
|
||
```ts | ||
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<Cat | Dog, 'dog'> // expected to be `Dog` | ||
``` | ||
|
||
该题比较简单,只要学会灵活使用 `infer` 与 `extends` 即可: | ||
|
||
```ts | ||
// 本题答案 | ||
type LookUp<T, P> = T extends { | ||
type: infer U | ||
} ? ( | ||
U extends P ? T : never | ||
) : never | ||
``` | ||
|
||
联合类型的判断是一个个来的,所以我们只要针对每一个单独写判断就行了。上面的解法中,我们先利用 `extend` + `infer` 锁定 `T` 的类型是包含 `type` key 的对象,且将 `infer U` 指向了 `type`,所以在内部再利用三元运算符判断 `U extends P ?` 就能将 `type` 命中的类型挑出来。 | ||
|
||
笔者翻了下答案,发现还有一种更高级的解法: | ||
|
||
```ts | ||
// 本题答案 | ||
type LookUp<U extends { type: any }, T extends U['type']> = 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 思维。 | ||
|
||
### [Trim Left](https://github.com/type-challenges/type-challenges/blob/main/questions/00106-medium-trimleft/README.md) | ||
|
||
实现 `TrimLeft<T>`,将字符串左侧空格清空: | ||
|
||
```ts | ||
type trimed = TrimLeft<' Hello World '> // expected to be 'Hello World ' | ||
``` | ||
|
||
在 TS 处理这类问题只能用递归,不能用正则。比较容易想到的是下面的写法: | ||
|
||
```ts | ||
// 本题答案 | ||
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T | ||
``` | ||
|
||
即如果字符串前面包含空格,就把空格去了继续递归,否则返回字符串本身。掌握该题的关键是 `infer` 也能用在字符串内进行推导。 | ||
|
||
|
||
### [Trim](https://github.com/type-challenges/type-challenges/blob/main/questions/00108-medium-trim/README.md) | ||
|
||
实现 `Trim<T>`,将字符串左右两侧空格清空: | ||
|
||
```ts | ||
type trimmed = Trim<' Hello World '> // expected to be 'Hello World' | ||
``` | ||
|
||
这个问题简单的解法是,左右都 Trim 一下: | ||
|
||
```ts | ||
// 本题答案 | ||
type Trim<T extends string> = TrimLeft<TrimRight<T>> | ||
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T | ||
type TrimRight<T extends string> = T extends `${infer R} ` ? TrimRight<R> : T | ||
``` | ||
|
||
这个成本很低,性能也不差,因为单写 `TrimLeft` 与 `TrimRight` 都很简单。 | ||
|
||
如果不采用先 Left 后 Right 的做法,想要一次性完成,就要有一些 TS 思维了。比较笨的思路是 “如果左边有空格就切分左边,或者右边有空格就切分右边”,最后写出来一个复杂的三元表达式。比较优秀的思路是利用 TS 联合类型: | ||
|
||
```ts | ||
// 本题答案 | ||
type Trim<T extends string> = T extends ` ${infer R}` | `${infer R} ` ? Trim<R> : T | ||
``` | ||
|
||
`extends` 后面还可以跟联合类型,这样任意一个匹配都会走到 `Trim<R>` 递归里。这就是比较难说清楚的 TS 思维,如果没有它,你只能想到三元表达式,但一旦理解了联合类型还可以在 `extends` 里这么用,TS 帮你做了 N 元表达式的能力,那么写出来的代码就会非常清秀。 | ||
|
||
### [Capitalize](https://github.com/type-challenges/type-challenges/blob/main/questions/00110-medium-capitalize/README.md) | ||
|
||
实现 `Capitalize<T>` 将字符串第一个字母大写: | ||
|
||
```ts | ||
type capitalized = Capitalize<'hello world'> // expected to be 'Hello world' | ||
``` | ||
|
||
如果这是一道 JS 题那就简单到爆,可题目是 TS 的,我们需要再度切换为 TS 思维。 | ||
|
||
首先要知道利用基础函数 `Uppercase` 将单个字母转化为大写,然后配合 `infer` 就不用多说了: | ||
|
||
```ts | ||
type MyCapitalize<T extends string> = T extends `${infer F}${infer U}` ? `${Uppercase<F>}${U}` : T | ||
``` | ||
|
||
### [Replace](https://github.com/type-challenges/type-challenges/blob/main/questions/00116-medium-replace/README.md) | ||
|
||
实现 TS 版函数 `Replace<S, From, To>`,将字符串 `From` 替换为 `To`: | ||
|
||
```ts | ||
type replaced = Replace<'types are fun!', 'fun', 'awesome'> // expected to be 'types are awesome!' | ||
``` | ||
|
||
把 `From` 夹在字符串中间,前后用两个 `infer` 推导,最后输出时前后不变,把 `From` 换成 `To` 就行了: | ||
|
||
```ts | ||
// 本题答案 | ||
type Replace<S extends string, From extends string, To extends string,> = | ||
S extends `${infer A}${From}${infer B}` ? `${A}${To}${B}` : S | ||
``` | ||
|
||
### [ReplaceAll](https://github.com/type-challenges/type-challenges/blob/main/questions/00119-medium-replaceall/README.md) | ||
|
||
实现 `ReplaceAll<S, From, To>`,将字符串 `From` 替换为 `To`: | ||
|
||
```ts | ||
type replaced = ReplaceAll<'t y p e s', ' ', ''> // expected to be 'types' | ||
``` | ||
|
||
该题与上题不同之处在于替换全部,解法肯定是递归,关键是何时递归的判断条件是什么。经过一番思考,如果 `infer From` 能匹配到不就说明还可以递归吗?所以加一层三元判断 `From extends ''` 即可: | ||
|
||
```ts | ||
// 本题答案 | ||
type ReplaceAll<S extends string, From extends string, To extends string> = | ||
S extends `${infer A}${From}${infer B}` ? ( | ||
From extends '' ? `${A}${To}${B}` : ReplaceAll<`${A}${To}${B}`, From, To> | ||
) : S | ||
``` | ||
|
||
### [Append Argument](https://github.com/type-challenges/type-challenges/blob/main/questions/00191-medium-append-argument/README.md) | ||
|
||
实现类型 `AppendArgument<F, E>`,将函数参数拓展一个: | ||
|
||
```ts | ||
type Fn = (a: number, b: string) => number | ||
type Result = AppendArgument<Fn, boolean> | ||
// expected be (a: number, b: string, x: boolean) => number | ||
``` | ||
|
||
该题很简单,用 `infer` 就行了: | ||
|
||
```ts | ||
// 本题答案 | ||
type AppendArgument<F, E> = F extends (...args: infer T) => infer R ? (...args: [...T, E]) => R : F | ||
``` | ||
|
||
## 总结 | ||
|
||
这几道题都比较简单,主要考察对 `infer` 和递归的熟练使用。 | ||
|
||
> 讨论地址是:[精读《Promise.all, Replace, Type Lookup...》· Issue #425 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/425) | ||
|
||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** | ||
|
||
> 关注 **前端精读微信公众号** | ||
|
||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg"> | ||
|
||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters