-
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
3 changed files
with
357 additions
and
3 deletions.
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
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,353 @@ | ||
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 49~56 题。 | ||
|
||
## 精读 | ||
|
||
### [Flip](https://github.com/type-challenges/type-challenges/blob/main/questions/04179-medium-flip/README.md) | ||
|
||
实现 `Flip<T>`,将对象 `T` 中 Key 与 Value 对调: | ||
|
||
```ts | ||
Flip<{ a: "x", b: "y", c: "z" }>; // {x: 'a', y: 'b', z: 'c'} | ||
Flip<{ a: 1, b: 2, c: 3 }>; // {1: 'a', 2: 'b', 3: 'c'} | ||
Flip<{ a: false, b: true }>; // {false: 'a', true: 'b'} | ||
``` | ||
|
||
在 `keyof` 描述对象时可以通过 `as` 追加变形,所以这道题应该这样处理: | ||
|
||
```ts | ||
type Flip<T> = { | ||
[K in keyof T as T[K]]: K | ||
} | ||
``` | ||
由于 Key 位置只能是 String or Number,所以 `T[K]` 描述 Key 会显示错误,我们需要限定 Value 的类型: | ||
```ts | ||
type Flip<T extends Record<string, string | number>> = { | ||
[K in keyof T as T[K]]: K | ||
} | ||
``` | ||
但这个答案无法通过测试用例 `Flip<{ pi: 3.14; bool: true }>`,原因是 `true` 不能作为 Key。只能用字符串 `'true'` 作为 Key,所以我们得强行把 Key 位置转化为字符串: | ||
```ts | ||
// 本题答案 | ||
type Flip<T extends Record<string, string | number | boolean>> = { | ||
[K in keyof T as `${T[K]}`]: K | ||
} | ||
``` | ||
### [Fibonacci Sequence](https://github.com/type-challenges/type-challenges/blob/main/questions/04182-medium-fibonacci-sequence/README.md) | ||
用 TS 实现斐波那契数列计算: | ||
```ts | ||
type Result1 = Fibonacci<3> // 2 | ||
type Result2 = Fibonacci<8> // 21 | ||
``` | ||
由于测试用例没有特别大的 Case,我们可以放心用递归实现。JS 版的斐波那契非常自然,但 TS 版我们只能用数组长度模拟计算,代码写起来自然会比较扭曲。 | ||
首先需要一个额外变量标记递归了多少次,递归到第 N 次结束: | ||
```ts | ||
type Fibonacci<T extends number, N = [1]> = N['length'] extends T ? ( | ||
// xxx | ||
) : Fibonacci<T, [...N, 1]> | ||
``` | ||
上面代码每次执行都判断是否递归完成,否则继续递归并把计数器加一。我们还需要一个数组存储答案,一个数组存储上一个数: | ||
```ts | ||
// 本题答案 | ||
type Fibonacci< | ||
T extends number, | ||
N extends number[] = [1], | ||
Prev extends number[] = [1], | ||
Cur extends number[] = [1] | ||
> = N['length'] extends T | ||
? Prev['length'] | ||
: Fibonacci<T, [...N, 1], Cur, [...Prev, ...Cur]> | ||
``` | ||
递归时拿 `Cur` 代替下次的 `Prev`,用 `[...Prev, ...Cur]` 代替下次的 `Cur`,也就是说,下次的 `Cur` 符合斐波那契定义。 | ||
### [AllCombinations](https://github.com/type-challenges/type-challenges/blob/main/questions/04260-medium-nomiwase/README.md) | ||
实现 `AllCombinations<S>` 对字符串 `S` 全排列: | ||
```ts | ||
type AllCombinations_ABC = AllCombinations<'ABC'> | ||
// should be '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA' | ||
``` | ||
首先要把 `ABC` 字符串拆成一个个独立的联合类型,进行二次组合才可能完成全排列: | ||
```ts | ||
type StrToUnion<S> = S extends `${infer F}${infer R}` | ||
? F | StrToUnion<R> | ||
: never | ||
``` | ||
`infer` 描述字符串时,第一个指向第一个字母,第二个指向剩余字母;对剩余字符串递归可以将其逐一拆解为单个字符并用 `|` 连接: | ||
```ts | ||
StrToUnion<'ABC'> // 'A' | 'B' | 'C' | ||
``` | ||
将 `StrToUnion<'ABC'>` 的结果记为 `U`,则利用对象转联合类型特征,可以制造出 `ABC` 在三个字母时的全排列: | ||
```ts | ||
{ [K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U] // `ABC${any}` | `ACB${any}` | `BAC${any}` | `BCA${any}` | `CAB${any}` | `CBA${any}` | ||
``` | ||
然而只要在每次递归时巧妙的加上 `'' |` 就可以直接得到答案了: | ||
```ts | ||
type AllCombinations<S extends string, U extends string = StrToUnion<S>> = | ||
| '' | ||
| { [K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U] // '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA' | ||
``` | ||
为什么这么神奇呢?这是因为每次递归时都会经历 `''`、`'A'`、`'AB'`、`'ABC'` 这样逐渐累加字符的过程,而每次都会遇到 `'' |` 使其自然形成了联合类型,比如遇到 `'A'` 时,会自然形成 `'A'` 这项联合类型,同时继续用 `'A'` 与 `Exclude<'A' | 'B' | 'C', 'A'>` 进行组合。 | ||
更精妙的是,第一次执行时的 `''` 填补了全排列的第一个 Case。 | ||
最后注意到上面的结果产生了一个 Error:"Type instantiation is excessively deep and possibly infinite",即这样递归可能产生死循环,因为 `Exclude<U, K>` 的结果可能是 `never`,所以最后在开头修补一下对 `never` 的判否,利用之前学习的知识,`never` 不会进行联合类型展开,所以我们用 `[never]` 判断来规避: | ||
```ts | ||
// 本题答案 | ||
type AllCombinations<S extends string, U extends string = StrToUnion<S>> = [ | ||
U | ||
] extends [never] | ||
? '' | ||
: '' | { [K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U] | ||
``` | ||
### [Greater Than](https://github.com/type-challenges/type-challenges/blob/main/questions/04425-medium-greater-than/README.md) | ||
实现 `GreaterThan<T, U>` 判断 `T > U`: | ||
```ts | ||
GreaterThan<2, 1> //should be true | ||
GreaterThan<1, 1> //should be false | ||
GreaterThan<10, 100> //should be false | ||
GreaterThan<111, 11> //should be true | ||
``` | ||
因为 TS 不支持加减法与大小判断,看到这道题时就应该想到有两种做法,一种是递归,但会受限于入参数量限制,可能堆栈溢出,一种是参考 [MinusOne](https://github.com/ascoders/weekly/blob/master/TS%20%E7%B1%BB%E5%9E%8B%E4%BD%93%E6%93%8D/248.%E7%B2%BE%E8%AF%BB%E3%80%8AMinusOne%2C%20PickByType%2C%20StartsWith...%E3%80%8B.md) 的特殊方法,用巧妙的方式构造出长度符合预期的数组,用数组 `['length']` 进行比较。 | ||
先说第一种,递归肯定要有一个递增 Key,拿 `T` `U` 先后进行对比,谁先追上这个数,谁就是较小的那个: | ||
```ts | ||
// 本题答案 | ||
type GreaterThan<T, U, R extends number[] = []> = T extends R['length'] | ||
? false | ||
: U extends R['length'] | ||
? true | ||
: GreaterThan<T, U, [...R, 1]> | ||
``` | ||
另一种做法是快速构造两个长度分别等于 `T` `U` 的数组,用数组快速判断谁更长。构造方式不再展开,参考 `MinusOne` 那篇的方法即可,重点说下如何快速判断 `[1, 1]` 与 `[1, 1, 1]` 谁更大。 | ||
因为 TS 没有大小判断能力,所以拿到了 `['length']` 也没有用,我们得考虑 `arr1 extends arr2` 这种方式。可惜的是,长度不相等的数组,`extends` 永远等于 `false`: | ||
```ts | ||
[1,1,1,1] extends [1,1,1] ? true : false // false | ||
[1,1,1] extends [1,1,1,1] ? true : false // false | ||
[1,1,1] extends [1,1,1] ? true : false // true | ||
``` | ||
但我们期望进行如下判断: | ||
```ts | ||
ArrGreaterThan<[1,1,1,1],[1,1,1]> // true | ||
ArrGreaterThan<[1,1,1],[1,1,1,1]> // false | ||
ArrGreaterThan<[1,1,1],[1,1,1]> // false | ||
``` | ||
解决方法非常体现 TS 思维:既然俩数组相等才返回 `true`,那我们用 `[...T, ...any]` 进行补充判定,如果能判定为 `true`,就说明前者长度更短(因为后者补充几项后可以判等): | ||
```ts | ||
type ArrGreaterThan<T extends 1[], U extends 1[]> = U extends [...T, ...any] | ||
? false | ||
: true | ||
``` | ||
这样一来,第二种答案就是这样的: | ||
```ts | ||
// 本题答案 | ||
type GreaterThan<T extends number, U extends number> = ArrGreaterThan< | ||
NumberToArr<T>, | ||
NumberToArr<U> | ||
> | ||
``` | ||
### [Zip](https://github.com/type-challenges/type-challenges/blob/main/questions/04471-medium-zip/README.md) | ||
实现 TS 版 `Zip` 函数: | ||
```ts | ||
type exp = Zip<[1, 2], [true, false]> // expected to be [[1, true], [2, false]] | ||
``` | ||
此题同样配合辅助变量,进行计数递归,并额外用一个类型变量存储结果: | ||
```ts | ||
// 本题答案 | ||
type Zip< | ||
T extends any[], | ||
U extends any[], | ||
I extends number[] = [], | ||
R extends any[] = [] | ||
> = I['length'] extends T['length'] | ||
? R | ||
: U[I['length']] extends undefined | ||
? Zip<T, U, [...I, 0], R> | ||
: Zip<T, U, [...I, 0], [...R, [T[I['length']], U[I['length']]]]> | ||
``` | ||
`[...R, [T[I['length']], U[I['length']]]]` 在每次递归时按照 Zip 规则添加一条结果,其中 `I['length']` 起到的作用类似 for 循环的下标 i,只是在 TS 语法中,我们只能用数组的方式模拟这种计数。 | ||
### [IsTuple](https://github.com/type-challenges/type-challenges/blob/main/questions/04484-medium-istuple/README.md) | ||
实现 `IsTuple<T>` 判断 `T` 是否为元组类型(Tuple): | ||
```ts | ||
type case1 = IsTuple<[number]> // true | ||
type case2 = IsTuple<readonly [number]> // true | ||
type case3 = IsTuple<number[]> // false | ||
``` | ||
不得不吐槽的是,无论是 TS 内部或者词法解析都是更有效的判断方式,但如果用 TS 来实现,就要换一种思路了。 | ||
Tuple 与 Array 在 TS 里的区别是前者长度有限,后者长度无限,从结果来看,如果访问其 `['length']` 属性,前者一定是一个固定数字,而后者返回 `number`,用这个特性判断即可: | ||
```ts | ||
// 本题答案 | ||
type IsTuple<T> = [T] extends [never] | ||
? false | ||
: T extends readonly any[] | ||
? number extends T['length'] | ||
? false | ||
: true | ||
: false | ||
``` | ||
其实这个答案是根据单测一点点试出来的,因为存在 `IsTuple<{ length: 1 }>` 单测用例,它可以通过 `number extends T['length']` 的校验,但因为其本身不是数组类型,所以无法通过 `T extends readonly any[]` 的前置判断。 | ||
### [Chunk](https://github.com/type-challenges/type-challenges/blob/main/questions/04499-medium-chunk/README.md) | ||
实现 TS 版 `Chunk`: | ||
```ts | ||
type exp1 = Chunk<[1, 2, 3], 2> // expected to be [[1, 2], [3]] | ||
type exp2 = Chunk<[1, 2, 3], 4> // expected to be [[1, 2, 3]] | ||
type exp3 = Chunk<[1, 2, 3], 1> // expected to be [[1], [2], [3]] | ||
``` | ||
老办法还是要递归,需要一个变量记录当前收集到 Chunk 里的内容,在 Chunk 达到上限时释放出来,同时也要注意未达到上限就结束时也要释放出来。 | ||
```ts | ||
type Chunk< | ||
T extends any[], | ||
N extends number = 1, | ||
Chunked extends any[] = [] | ||
> = T extends [infer First, ...infer Last] | ||
? Chunked['length'] extends N | ||
? [Chunked, ...Chunk<T, N>] | ||
: Chunk<Last, N, [...Chunked, First]> | ||
: [Chunked] | ||
``` | ||
`Chunked['length'] extends N` 判断 `Chunked` 数组长度达到 `N` 后就释放出来,否则把当前数组第一项 `First` 继续塞到 `Chunked` 数组,数组项从 `Last` 开始继续递归。 | ||
我们发现 `Chunk<[], 1>` 这个单测没过,因为当 `Chunked` 没有项目时,就无需成组了,所以完整的答案是: | ||
```ts | ||
// 本题答案 | ||
type Chunk< | ||
T extends any[], | ||
N extends number = 1, | ||
Chunked extends any[] = [] | ||
> = T extends [infer Head, ...infer Tail] | ||
? Chunked['length'] extends N | ||
? [Chunked, ...Chunk<T, N>] | ||
: Chunk<Tail, N, [...Chunked, Head]> | ||
: Chunked extends [] | ||
? Chunked | ||
: [Chunked] | ||
``` | ||
### [Fill](https://github.com/type-challenges/type-challenges/blob/main/questions/04518-medium-fill/README.md) | ||
实现 `Fill<T, N, Start?, End?>`,将数组 `T` 的每一项替换为 `N`: | ||
```ts | ||
type exp = Fill<[1, 2, 3], 0> // expected to be [0, 0, 0] | ||
``` | ||
这道题也需要用递归 + Flag 方式解决,即定义一个 `I` 表示当前递归的下标,一个 `Flag` 表示是否到了要替换的下标,只要到了这个下标,该 `Flag` 就永远为 `true`: | ||
```ts | ||
type Fill< | ||
T extends unknown[], | ||
N, | ||
Start extends number = 0, | ||
End extends number = T['length'], | ||
I extends any[] = [], | ||
Flag extends boolean = I['length'] extends Start ? true : false | ||
> | ||
``` | ||
由于递归会不断生成完整答案,我们将 `T` 定义为可变的,即每次仅处理第一条,如果当前 `Flag` 为 `true` 就采用替换值 `N`,否则就拿原本的第一个字符: | ||
```ts | ||
type Fill< | ||
T extends unknown[], | ||
N, | ||
Start extends number = 0, | ||
End extends number = T['length'], | ||
I extends any[] = [], | ||
Flag extends boolean = I['length'] extends Start ? true : false | ||
> = I['length'] extends End | ||
? T | ||
: T extends [infer F, ...infer R] | ||
? Flag extends false | ||
? [F, ...Fill<R, N, Start, End, [...I, 0]>] | ||
: [N, ...Fill<R, N, Start, End, [...I, 0]>] | ||
: T | ||
``` | ||
但这个答案没有通过测试,仔细想想发现 `Flag` 在 `I` 长度超过 `Start` 后就判定失败了,为了让超过后维持 `true`,在 `Flag` 为 `true` 时将其传入覆盖后续值即可: | ||
```ts | ||
// 本题答案 | ||
type Fill< | ||
T extends unknown[], | ||
N, | ||
Start extends number = 0, | ||
End extends number = T['length'], | ||
I extends any[] = [], | ||
Flag extends boolean = I['length'] extends Start ? true : false | ||
> = I['length'] extends End | ||
? T | ||
: T extends [infer F, ...infer R] | ||
? Flag extends false | ||
? [F, ...Fill<R, N, Start, End, [...I, 0]>] | ||
: [N, ...Fill<R, N, Start, End, [...I, 0], Flag>] | ||
: T | ||
``` | ||
## 总结 | ||
> 讨论地址是:[精读《Flip, Fibonacci, AllCombinations...》· Issue #432 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/432) | ||
**如果你想参与讨论,请 [点击这里](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