-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
272 additions
and
1 deletion.
There are no files selected for viewing
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,270 @@ | ||
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 17~24 题。 | ||
|
||
## 精读 | ||
|
||
### [Permutation](https://github.com/type-challenges/type-challenges/blob/main/questions/00296-medium-permutation/README.md) | ||
|
||
实现 `Permutation` 类型,将联合类型替换为可能的全排列: | ||
|
||
```ts | ||
type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A'] | ||
``` | ||
|
||
看到这题立马联想到 TS 对多个联合类型泛型处理是采用分配律的,在第一次做到 `Exclude` 题目时遇到过: | ||
|
||
```ts | ||
Exclude<'a' | 'b', 'a' | 'c'> | ||
// 等价于 | ||
Exclude<'a', 'a' | 'c'> | Exclude<'b', 'a' | 'c'> | ||
``` | ||
|
||
所以这题如果能 “递归触发联合类型分配率”,就有戏解决啊。但触发的条件必须存在两个泛型,而题目传入的只有一个,我们只好创造第二个泛型,使其默认值等于第一个: | ||
|
||
```ts | ||
type Permutation<T, U = T> | ||
``` | ||
|
||
这样对本题来说,会做如下展开: | ||
|
||
```ts | ||
Permutation<'A' | 'B' | 'C'> | ||
// 等价于 | ||
Permutation<'A' | 'B' | 'C', 'A' | 'B' | 'C'> | ||
// 等价于 | ||
Permutation<'A', 'A' | 'B' | 'C'> | Permutation<'B', 'A' | 'B' | 'C'> | Permutation<'C', 'A' | 'B' | 'C'> | ||
``` | ||
|
||
对于 `Permutation<'A', 'A' | 'B' | 'C'>` 来说,排除掉对自身的组合,可形成 `'A', 'B'`,`'A', 'C'` 组合,之后只要再递归一次,再拼一次,把已有的排除掉,就形成了 `A` 的全排列,以此类推,形成所有字母的全排列。 | ||
|
||
这里要注意两点: | ||
|
||
1. 如何排除掉自身?`Exclude<T, P>` 正合适,该函数遇到 `T` 在联合类型 `P` 中时,会返回 `never`,否则返回 `T`。 | ||
2. 递归何时结束?每次递归时用 `Exclude<U, T>` 留下没用过的组合,最后一次组合用完一定会剩下 `never`,此时终止递归。 | ||
|
||
```ts | ||
// 本题答案 | ||
type Permutation<T, U = T> = [T] extends [never] ? [] : T extends U ? [T, ...Permutation<Exclude<U, T>>] : [] | ||
``` | ||
|
||
验证一下答案,首先展开 `Permutation<'A', 'B', 'C'>`: | ||
|
||
```ts | ||
'A' extends 'A' | 'B' | 'C' ? ['A', ...Permutation<'B' | 'C'>] : [] | ||
'B' extends 'A' | 'B' | 'C' ? ['B', ...Permutation<'A' | 'C'>] : [] | ||
'C' extends 'A' | 'B' | 'C' ? ['C', ...Permutation<'A' | 'B'>] : [] | ||
``` | ||
|
||
我们再展开第一行 `Permutation<'B' | 'C'>`: | ||
|
||
```ts | ||
'B' extends 'B' | 'C' ? ['B', ...Permutation<'C'>] : [] | ||
'C' extends 'B' | 'C' ? ['C', ...Permutation<'B'>] : [] | ||
``` | ||
|
||
再展开第一行的 `Permutation<'C'>`: | ||
|
||
```ts | ||
'C' extends 'C' ? ['C', ...Permutation<never>] : [] | ||
``` | ||
|
||
此时已经完成全排列,但我们还要处理一下 `Permutation<never>`,使其返回 `[]` 并终止递归。那为什么要用 `[T] extends [never]` 而不是 `T extends never` 呢? | ||
|
||
如果我们用 `T extends never` 代替本题答案,输出结果是 `never`,原因如下: | ||
|
||
```ts | ||
type X = never extends never ? 1 : 0 // 1 | ||
type Custom<T> = T extends never ? 1 : 0 | ||
type Y = Custom<never> // never | ||
``` | ||
|
||
理论上相同的代码,为什么用泛型后输出就变成 `never` 了呢?原因是 TS 在做 `T extends never ?` 时,会对联合类型进行分配,此时有一个特例,即当 `T = never` 时,会跳过分配直接返回 `T` 本身,所以三元判断代码实际上没有执行。 | ||
|
||
`[T] extends [never]` 这种写法可以避免 TS 对联合类型进行分配,继而绕过上面的问题。 | ||
|
||
### [Length of String](https://github.com/type-challenges/type-challenges/blob/main/questions/00298-medium-length-of-string/README.md) | ||
|
||
实现 `LengthOfString<T>` 返回字符串 T 的长度: | ||
|
||
```ts | ||
LengthOfString<'abc'> // 3 | ||
``` | ||
|
||
破解此题你需要知道一个前提,即 TS 访问数组类型的 `[length]` 属性可以拿到长度值: | ||
|
||
```ts | ||
['a','b','c']['length'] // 3 | ||
``` | ||
|
||
也就是说,我们需要把 `'abc'` 转化为 `['a', 'b', 'c']`。 | ||
|
||
第二个需要了解的前置知识是,用 `infer` 指代字符串时,第一个指代指向第一个字母,第二个指向其余所有字母: | ||
|
||
```ts | ||
'abc' extends `${infer S}${infer E}` ? S : never // 'a' | ||
``` | ||
|
||
那转换后的数组存在哪呢?类似 js,我们弄第二个默认值泛型存储即可: | ||
|
||
```ts | ||
// 本题答案 | ||
type LengthOfString<S, N extends any[] = []> = S extends `${infer S}${infer E}` ? LengthOfString<E, [...N, S]> : N['length'] | ||
``` | ||
|
||
思路就是,每次把字符串第一个字母拿出来放到数组 `N` 的第一项,直到字符串被取完,直接拿此时的数组长度。 | ||
|
||
### [Flatten](https://github.com/type-challenges/type-challenges/blob/main/questions/00459-medium-flatten/README.md) | ||
|
||
实现类型 `Flatten`: | ||
|
||
```ts | ||
type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5] | ||
``` | ||
|
||
此题一看就需要递归: | ||
|
||
```ts | ||
// 本题答案 | ||
type Flatten<T extends any[], Result extends any[] = []> = T extends [infer Start, ...infer Rest] ? ( | ||
Start extends any[] ? Flatten<Rest, [...Result, ...Flatten<Start>]> : Flatten<Rest, [...Result, Start]> | ||
) : Result | ||
``` | ||
|
||
这道题看似答案复杂,其实还是用到了上一题的套路:**递归时如果需要存储临时变量,用泛型默认值来存储**。 | ||
|
||
本题我们就用 `Result` 这个泛型存储打平后的结果,每次拿到数组第一个值,如果第一个值不是数组,则直接存进去继续递归,此时 `T` 自然是剩余的 `Rest`;如果第一个值是数组,则将其打平,此时有个精彩的地方,即 `...Start` 打平后依然可能是数组,比如 `[[5]]` 就套了两层,能不能想到 `...Flatten<Start>` 继续复用递归是解题关键。 | ||
|
||
### [Append to object](https://github.com/type-challenges/type-challenges/blob/main/questions/00527-medium-append-to-object/README.md) | ||
|
||
实现 `AppendToObject`: | ||
|
||
```ts | ||
type Test = { id: '1' } | ||
type Result = AppendToObject<Test, 'value', 4> // expected to be { id: '1', value: 4 } | ||
``` | ||
|
||
结合之前刷题的经验,该题解法很简单,注意 `K in Key` 可以给对象拓展某些指定 Key: | ||
|
||
```ts | ||
// 本题答案 | ||
type AppendToObject<Obj, Key extends string, Value> = Obj & { | ||
[K in Key]: Value | ||
} | ||
``` | ||
|
||
当然也有不用 `Obj &` 的写法,即把原始对象和新 Key, Value 合在一起的描述方式: | ||
|
||
```ts | ||
// 本题答案 | ||
type AppendToObject<T, U extends number | string | symbol, V> = { | ||
[key in (keyof T) | U]: key extends U ? V : T[Exclude<key, U>] | ||
} | ||
``` | ||
|
||
### [Absolute](https://github.com/type-challenges/type-challenges/blob/main/questions/00529-medium-absolute/README.md) | ||
|
||
实现 `Absolute` 将数字转成绝对值: | ||
|
||
```ts | ||
type Test = -100; | ||
type Result = Absolute<Test>; // expected to be "100" | ||
``` | ||
|
||
该题重点是把数字转成绝对值字符串,所以我们可以用字符串的方式进行匹配: | ||
|
||
```ts | ||
// 本题答案 | ||
type Absolute<T extends number> = `${T}` extends `-${infer R}` ? R : `${T}` | ||
``` | ||
|
||
为什么不用 `T extends` 来判断呢?因为 `T` 是数字,这样写无法匹配符号的字符串描述。 | ||
|
||
### [String to Union](https://github.com/type-challenges/type-challenges/blob/main/questions/00531-medium-string-to-union/README.md) | ||
|
||
实现 `StringToUnion` 将字符串转换为联合类型: | ||
|
||
```ts | ||
type Test = '123'; | ||
type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3" | ||
``` | ||
|
||
还是老套路,用一个新的泛型存储答案,递归即可: | ||
|
||
```ts | ||
// 本题答案 | ||
type StringToUnion<T, P = never> = T extends `${infer F}${infer R}` ? StringToUnion<R, P | F> : P | ||
``` | ||
|
||
当然也可以不依托泛型存储答案,因为该题比较特殊,可以直接用 `|`: | ||
|
||
```ts | ||
// 本题答案 | ||
type StringToUnion<T> = T extends `${infer F}${infer R}` ? F | StringToUnion<R> : never | ||
``` | ||
|
||
### [Merge](https://github.com/type-challenges/type-challenges/blob/main/questions/00599-medium-merge/README.md) | ||
|
||
实现 `Merge` 合并两个对象,冲突时后者优先: | ||
|
||
```ts | ||
type foo = { | ||
name: string; | ||
age: string; | ||
} | ||
type coo = { | ||
age: number; | ||
sex: string | ||
} | ||
type Result = Merge<foo,coo>; // expected to be {name: string, age: number, sex: string} | ||
``` | ||
|
||
这道题答案甚至是之前题目的解题步骤,即用一个对象描述 + `keyof` 的思维: | ||
|
||
```ts | ||
// 本题答案 | ||
type Merge<A extends object, B extends object> = { | ||
[K in keyof A | keyof B] : K extends keyof B ? B[K] : ( | ||
K extends keyof A ? A[K] : never | ||
) | ||
} | ||
``` | ||
|
||
只要知道 `in keyof` 支持元组,值部分用 `extends` 进行区分即可,很简单。 | ||
|
||
### [KebabCase](https://github.com/type-challenges/type-challenges/blob/main/questions/00612-medium-kebabcase/README.md) | ||
|
||
实现驼峰转横线的函数 `KebabCase`: | ||
|
||
```ts | ||
KebabCase<'FooBarBaz'> // 'foo-bar-baz' | ||
``` | ||
|
||
还是老套路,用第二个参数存储结果,用递归的方式遍历字符串,遇到大写字母就转成小写并添加上 `-`,最后把开头的 `-` 干掉就行了: | ||
|
||
```ts | ||
// 本题答案 | ||
type KebabCase<S, U extends string = ''> = S extends `${infer F}${infer R}` ? ( | ||
Lowercase<F> extends F ? KebabCase<R, `${U}${F}`> : KebabCase<R, `${U}-${Lowercase<F>}`> | ||
) : RemoveFirstHyphen<U> | ||
type RemoveFirstHyphen<S> = S extends `-${infer Rest}` ? Rest : S | ||
``` | ||
|
||
分开写就非常容易懂了,首先 `KebabCase` 每次递归取第一个字符,如何判断这个字符是大写呢?只要小写不等于原始值就是大写,所以判断条件就是 `Lowercase<F> extends F` 的 false 分支。然后再写个函数 `RemoveFirstHyphen` 把字符串第一个 `-` 干掉即可。 | ||
|
||
## 总结 | ||
|
||
TS 是一门编程语言,而不是一门简单的描述或者修饰符,很多复杂类型问题要动用逻辑思维来实现,而不是查查语法就能简单实现。 | ||
|
||
> 讨论地址是:[精读《Permutation, Flatten, Absolute...》· Issue #426 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/426) | ||
|
||
**如果你想参与讨论,请 [点击这里](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