Skip to content

Commit

Permalink
425
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed Jun 20, 2022
1 parent 183ba72 commit 0e87eef
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 1 deletion.
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 还是 featureTS`{ [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` 都很简单。

如果不采用先 LeftRight 的做法,想要一次性完成,就要有一些 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))


3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

前端界的好文精读,每周更新!

最新精读:<a href="./TS 类型体操/244.%E7%B2%BE%E8%AF%BB%E3%80%8AGet%20return%20type%2C%20Omit%2C%20ReadOnly...%E3%80%8B.md">244.精读《Get return type, Omit, ReadOnly...》</a>
最新精读:<a href="./TS 类型体操/245.%E7%B2%BE%E8%AF%BB%E3%80%8APromise.all%2C%20Replace%2C%20Type%20Lookup...%E3%80%8B.md">245.精读《Promise.all, Replace, Type Lookup...》</a>

素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)

Expand Down Expand Up @@ -196,6 +196,7 @@

- <a href="./TS 类型体操/243.%E7%B2%BE%E8%AF%BB%E3%80%8APick%2C%20Awaited%2C%20If%E3%80%8B.md">243.精读《Pick, Awaited, If》</a>
- <a href="./TS 类型体操/244.%E7%B2%BE%E8%AF%BB%E3%80%8AGet%20return%20type%2C%20Omit%2C%20ReadOnly...%E3%80%8B.md">244.精读《Get return type, Omit, ReadOnly...》</a>
- <a href="./TS 类型体操/245.%E7%B2%BE%E8%AF%BB%E3%80%8APromise.all%2C%20Replace%2C%20Type%20Lookup...%E3%80%8B.md">245.精读《Promise.all, Replace, Type Lookup...》</a>

### 设计模式

Expand Down

0 comments on commit 0e87eef

Please sign in to comment.