Skip to content

Commit 321893b

Browse files
committed
250
1 parent a88290a commit 321893b

File tree

3 files changed

+357
-3
lines changed

3 files changed

+357
-3
lines changed

TS 类型体操/243.精读《Pick, Awaited, If》.md renamed to TS 类型体操/243.精读《Pick, Awaited, If...》.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ type Parameters<T> = T extends (...args: infer P) => any ? P : []
351351
352352
学会 TS 基础语法后,活用才是关键。
353353
354-
> 讨论地址是:[精读《Pick, Awaited, If》· Issue #422 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/422)
354+
> 讨论地址是:[精读《Pick, Awaited, If...》· Issue #422 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/422)
355355
356356
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
357357
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 49~56 题。
2+
3+
## 精读
4+
5+
### [Flip](https://github.com/type-challenges/type-challenges/blob/main/questions/04179-medium-flip/README.md)
6+
7+
实现 `Flip<T>`,将对象 `T` 中 Key 与 Value 对调:
8+
9+
```ts
10+
Flip<{ a: "x", b: "y", c: "z" }>; // {x: 'a', y: 'b', z: 'c'}
11+
Flip<{ a: 1, b: 2, c: 3 }>; // {1: 'a', 2: 'b', 3: 'c'}
12+
Flip<{ a: false, b: true }>; // {false: 'a', true: 'b'}
13+
```
14+
15+
`keyof` 描述对象时可以通过 `as` 追加变形,所以这道题应该这样处理:
16+
17+
```ts
18+
type Flip<T> = {
19+
[K in keyof T as T[K]]: K
20+
}
21+
```
22+
23+
由于 Key 位置只能是 String or Number,所以 `T[K]` 描述 Key 会显示错误,我们需要限定 Value 的类型:
24+
25+
```ts
26+
type Flip<T extends Record<string, string | number>> = {
27+
[K in keyof T as T[K]]: K
28+
}
29+
```
30+
31+
但这个答案无法通过测试用例 `Flip<{ pi: 3.14; bool: true }>`,原因是 `true` 不能作为 Key。只能用字符串 `'true'` 作为 Key,所以我们得强行把 Key 位置转化为字符串:
32+
33+
```ts
34+
// 本题答案
35+
type Flip<T extends Record<string, string | number | boolean>> = {
36+
[K in keyof T as `${T[K]}`]: K
37+
}
38+
```
39+
40+
### [Fibonacci Sequence](https://github.com/type-challenges/type-challenges/blob/main/questions/04182-medium-fibonacci-sequence/README.md)
41+
42+
用 TS 实现斐波那契数列计算:
43+
44+
```ts
45+
type Result1 = Fibonacci<3> // 2
46+
type Result2 = Fibonacci<8> // 21
47+
```
48+
49+
由于测试用例没有特别大的 Case,我们可以放心用递归实现。JS 版的斐波那契非常自然,但 TS 版我们只能用数组长度模拟计算,代码写起来自然会比较扭曲。
50+
51+
首先需要一个额外变量标记递归了多少次,递归到第 N 次结束:
52+
53+
```ts
54+
type Fibonacci<T extends number, N = [1]> = N['length'] extends T ? (
55+
// xxx
56+
) : Fibonacci<T, [...N, 1]>
57+
```
58+
59+
上面代码每次执行都判断是否递归完成,否则继续递归并把计数器加一。我们还需要一个数组存储答案,一个数组存储上一个数:
60+
61+
```ts
62+
// 本题答案
63+
type Fibonacci<
64+
T extends number,
65+
N extends number[] = [1],
66+
Prev extends number[] = [1],
67+
Cur extends number[] = [1]
68+
> = N['length'] extends T
69+
? Prev['length']
70+
: Fibonacci<T, [...N, 1], Cur, [...Prev, ...Cur]>
71+
```
72+
73+
递归时拿 `Cur` 代替下次的 `Prev`,用 `[...Prev, ...Cur]` 代替下次的 `Cur`,也就是说,下次的 `Cur` 符合斐波那契定义。
74+
75+
### [AllCombinations](https://github.com/type-challenges/type-challenges/blob/main/questions/04260-medium-nomiwase/README.md)
76+
77+
实现 `AllCombinations<S>` 对字符串 `S` 全排列:
78+
79+
```ts
80+
type AllCombinations_ABC = AllCombinations<'ABC'>
81+
// should be '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA'
82+
```
83+
84+
首先要把 `ABC` 字符串拆成一个个独立的联合类型,进行二次组合才可能完成全排列:
85+
86+
```ts
87+
type StrToUnion<S> = S extends `${infer F}${infer R}`
88+
? F | StrToUnion<R>
89+
: never
90+
```
91+
92+
`infer` 描述字符串时,第一个指向第一个字母,第二个指向剩余字母;对剩余字符串递归可以将其逐一拆解为单个字符并用 `|` 连接:
93+
94+
```ts
95+
StrToUnion<'ABC'> // 'A' | 'B' | 'C'
96+
```
97+
98+
`StrToUnion<'ABC'>` 的结果记为 `U`,则利用对象转联合类型特征,可以制造出 `ABC` 在三个字母时的全排列:
99+
100+
```ts
101+
{ [K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U] // `ABC${any}` | `ACB${any}` | `BAC${any}` | `BCA${any}` | `CAB${any}` | `CBA${any}`
102+
```
103+
104+
然而只要在每次递归时巧妙的加上 `'' |` 就可以直接得到答案了:
105+
106+
```ts
107+
type AllCombinations<S extends string, U extends string = StrToUnion<S>> =
108+
| ''
109+
| { [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'
110+
```
111+
112+
为什么这么神奇呢?这是因为每次递归时都会经历 `''``'A'``'AB'``'ABC'` 这样逐渐累加字符的过程,而每次都会遇到 `'' |` 使其自然形成了联合类型,比如遇到 `'A'` 时,会自然形成 `'A'` 这项联合类型,同时继续用 `'A'``Exclude<'A' | 'B' | 'C', 'A'>` 进行组合。
113+
114+
更精妙的是,第一次执行时的 `''` 填补了全排列的第一个 Case。
115+
116+
最后注意到上面的结果产生了一个 Error:"Type instantiation is excessively deep and possibly infinite",即这样递归可能产生死循环,因为 `Exclude<U, K>` 的结果可能是 `never`,所以最后在开头修补一下对 `never` 的判否,利用之前学习的知识,`never` 不会进行联合类型展开,所以我们用 `[never]` 判断来规避:
117+
118+
```ts
119+
// 本题答案
120+
type AllCombinations<S extends string, U extends string = StrToUnion<S>> = [
121+
U
122+
] extends [never]
123+
? ''
124+
: '' | { [K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U]
125+
```
126+
127+
### [Greater Than](https://github.com/type-challenges/type-challenges/blob/main/questions/04425-medium-greater-than/README.md)
128+
129+
实现 `GreaterThan<T, U>` 判断 `T > U`:
130+
131+
```ts
132+
GreaterThan<2, 1> //should be true
133+
GreaterThan<1, 1> //should be false
134+
GreaterThan<10, 100> //should be false
135+
GreaterThan<111, 11> //should be true
136+
```
137+
138+
因为 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']` 进行比较。
139+
140+
先说第一种,递归肯定要有一个递增 Key,拿 `T` `U` 先后进行对比,谁先追上这个数,谁就是较小的那个:
141+
142+
```ts
143+
// 本题答案
144+
type GreaterThan<T, U, R extends number[] = []> = T extends R['length']
145+
? false
146+
: U extends R['length']
147+
? true
148+
: GreaterThan<T, U, [...R, 1]>
149+
```
150+
151+
另一种做法是快速构造两个长度分别等于 `T` `U` 的数组,用数组快速判断谁更长。构造方式不再展开,参考 `MinusOne` 那篇的方法即可,重点说下如何快速判断 `[1, 1]``[1, 1, 1]` 谁更大。
152+
153+
因为 TS 没有大小判断能力,所以拿到了 `['length']` 也没有用,我们得考虑 `arr1 extends arr2` 这种方式。可惜的是,长度不相等的数组,`extends` 永远等于 `false`:
154+
155+
```ts
156+
[1,1,1,1] extends [1,1,1] ? true : false // false
157+
[1,1,1] extends [1,1,1,1] ? true : false // false
158+
[1,1,1] extends [1,1,1] ? true : false // true
159+
```
160+
161+
但我们期望进行如下判断:
162+
163+
```ts
164+
ArrGreaterThan<[1,1,1,1],[1,1,1]> // true
165+
ArrGreaterThan<[1,1,1],[1,1,1,1]> // false
166+
ArrGreaterThan<[1,1,1],[1,1,1]> // false
167+
```
168+
169+
解决方法非常体现 TS 思维:既然俩数组相等才返回 `true`,那我们用 `[...T, ...any]` 进行补充判定,如果能判定为 `true`,就说明前者长度更短(因为后者补充几项后可以判等):
170+
171+
```ts
172+
type ArrGreaterThan<T extends 1[], U extends 1[]> = U extends [...T, ...any]
173+
? false
174+
: true
175+
```
176+
177+
这样一来,第二种答案就是这样的:
178+
179+
```ts
180+
// 本题答案
181+
type GreaterThan<T extends number, U extends number> = ArrGreaterThan<
182+
NumberToArr<T>,
183+
NumberToArr<U>
184+
>
185+
```
186+
187+
### [Zip](https://github.com/type-challenges/type-challenges/blob/main/questions/04471-medium-zip/README.md)
188+
189+
实现 TS 版 `Zip` 函数:
190+
191+
```ts
192+
type exp = Zip<[1, 2], [true, false]> // expected to be [[1, true], [2, false]]
193+
```
194+
195+
此题同样配合辅助变量,进行计数递归,并额外用一个类型变量存储结果:
196+
197+
```ts
198+
// 本题答案
199+
type Zip<
200+
T extends any[],
201+
U extends any[],
202+
I extends number[] = [],
203+
R extends any[] = []
204+
> = I['length'] extends T['length']
205+
? R
206+
: U[I['length']] extends undefined
207+
? Zip<T, U, [...I, 0], R>
208+
: Zip<T, U, [...I, 0], [...R, [T[I['length']], U[I['length']]]]>
209+
```
210+
211+
`[...R, [T[I['length']], U[I['length']]]]` 在每次递归时按照 Zip 规则添加一条结果,其中 `I['length']` 起到的作用类似 for 循环的下标 i,只是在 TS 语法中,我们只能用数组的方式模拟这种计数。
212+
213+
### [IsTuple](https://github.com/type-challenges/type-challenges/blob/main/questions/04484-medium-istuple/README.md)
214+
215+
实现 `IsTuple<T>` 判断 `T` 是否为元组类型(Tuple):
216+
217+
```ts
218+
type case1 = IsTuple<[number]> // true
219+
type case2 = IsTuple<readonly [number]> // true
220+
type case3 = IsTuple<number[]> // false
221+
```
222+
223+
不得不吐槽的是,无论是 TS 内部或者词法解析都是更有效的判断方式,但如果用 TS 来实现,就要换一种思路了。
224+
225+
Tuple 与 Array 在 TS 里的区别是前者长度有限,后者长度无限,从结果来看,如果访问其 `['length']` 属性,前者一定是一个固定数字,而后者返回 `number`,用这个特性判断即可:
226+
227+
```ts
228+
// 本题答案
229+
type IsTuple<T> = [T] extends [never]
230+
? false
231+
: T extends readonly any[]
232+
? number extends T['length']
233+
? false
234+
: true
235+
: false
236+
```
237+
238+
其实这个答案是根据单测一点点试出来的,因为存在 `IsTuple<{ length: 1 }>` 单测用例,它可以通过 `number extends T['length']` 的校验,但因为其本身不是数组类型,所以无法通过 `T extends readonly any[]` 的前置判断。
239+
240+
### [Chunk](https://github.com/type-challenges/type-challenges/blob/main/questions/04499-medium-chunk/README.md)
241+
242+
实现 TS 版 `Chunk`:
243+
244+
```ts
245+
type exp1 = Chunk<[1, 2, 3], 2> // expected to be [[1, 2], [3]]
246+
type exp2 = Chunk<[1, 2, 3], 4> // expected to be [[1, 2, 3]]
247+
type exp3 = Chunk<[1, 2, 3], 1> // expected to be [[1], [2], [3]]
248+
```
249+
250+
老办法还是要递归,需要一个变量记录当前收集到 Chunk 里的内容,在 Chunk 达到上限时释放出来,同时也要注意未达到上限就结束时也要释放出来。
251+
252+
```ts
253+
type Chunk<
254+
T extends any[],
255+
N extends number = 1,
256+
Chunked extends any[] = []
257+
> = T extends [infer First, ...infer Last]
258+
? Chunked['length'] extends N
259+
? [Chunked, ...Chunk<T, N>]
260+
: Chunk<Last, N, [...Chunked, First]>
261+
: [Chunked]
262+
```
263+
264+
`Chunked['length'] extends N` 判断 `Chunked` 数组长度达到 `N` 后就释放出来,否则把当前数组第一项 `First` 继续塞到 `Chunked` 数组,数组项从 `Last` 开始继续递归。
265+
266+
我们发现 `Chunk<[], 1>` 这个单测没过,因为当 `Chunked` 没有项目时,就无需成组了,所以完整的答案是:
267+
268+
```ts
269+
// 本题答案
270+
type Chunk<
271+
T extends any[],
272+
N extends number = 1,
273+
Chunked extends any[] = []
274+
> = T extends [infer Head, ...infer Tail]
275+
? Chunked['length'] extends N
276+
? [Chunked, ...Chunk<T, N>]
277+
: Chunk<Tail, N, [...Chunked, Head]>
278+
: Chunked extends []
279+
? Chunked
280+
: [Chunked]
281+
```
282+
283+
### [Fill](https://github.com/type-challenges/type-challenges/blob/main/questions/04518-medium-fill/README.md)
284+
285+
实现 `Fill<T, N, Start?, End?>`,将数组 `T` 的每一项替换为 `N`
286+
287+
```ts
288+
type exp = Fill<[1, 2, 3], 0> // expected to be [0, 0, 0]
289+
```
290+
291+
这道题也需要用递归 + Flag 方式解决,即定义一个 `I` 表示当前递归的下标,一个 `Flag` 表示是否到了要替换的下标,只要到了这个下标,该 `Flag` 就永远为 `true`
292+
293+
```ts
294+
type Fill<
295+
T extends unknown[],
296+
N,
297+
Start extends number = 0,
298+
End extends number = T['length'],
299+
I extends any[] = [],
300+
Flag extends boolean = I['length'] extends Start ? true : false
301+
>
302+
```
303+
304+
由于递归会不断生成完整答案,我们将 `T` 定义为可变的,即每次仅处理第一条,如果当前 `Flag``true` 就采用替换值 `N`,否则就拿原本的第一个字符:
305+
306+
```ts
307+
type Fill<
308+
T extends unknown[],
309+
N,
310+
Start extends number = 0,
311+
End extends number = T['length'],
312+
I extends any[] = [],
313+
Flag extends boolean = I['length'] extends Start ? true : false
314+
> = I['length'] extends End
315+
? T
316+
: T extends [infer F, ...infer R]
317+
? Flag extends false
318+
? [F, ...Fill<R, N, Start, End, [...I, 0]>]
319+
: [N, ...Fill<R, N, Start, End, [...I, 0]>]
320+
: T
321+
```
322+
323+
但这个答案没有通过测试,仔细想想发现 `Flag``I` 长度超过 `Start` 后就判定失败了,为了让超过后维持 `true`,在 `Flag``true` 时将其传入覆盖后续值即可:
324+
325+
```ts
326+
// 本题答案
327+
type Fill<
328+
T extends unknown[],
329+
N,
330+
Start extends number = 0,
331+
End extends number = T['length'],
332+
I extends any[] = [],
333+
Flag extends boolean = I['length'] extends Start ? true : false
334+
> = I['length'] extends End
335+
? T
336+
: T extends [infer F, ...infer R]
337+
? Flag extends false
338+
? [F, ...Fill<R, N, Start, End, [...I, 0]>]
339+
: [N, ...Fill<R, N, Start, End, [...I, 0], Flag>]
340+
: T
341+
```
342+
343+
## 总结
344+
345+
> 讨论地址是:[精读《Flip, Fibonacci, AllCombinations...》· Issue #432 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/432)
346+
347+
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
348+
349+
> 关注 **前端精读微信公众号**
350+
351+
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
352+
353+
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))

readme.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

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

9-
最新精读:<a href="./TS 类型体操/249.%E7%B2%BE%E8%AF%BB%E3%80%8AObjectEntries%2C%20Shift%2C%20Reverse...%E3%80%8B.md">249.精读《ObjectEntries, Shift, Reverse...》</a>
9+
最新精读:<a href="./TS 类型体操/250.%E7%B2%BE%E8%AF%BB%E3%80%8AFlip%2C%20Fibonacci%2C%20AllCombinations...%E3%80%8B.md">250.精读《Flip, Fibonacci, AllCombinations...》</a>
1010

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

@@ -194,13 +194,14 @@
194194

195195
### TS 类型体操
196196

197-
- <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>
197+
- <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>
198198
- <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>
199199
- <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>
200200
- <a href="./TS 类型体操/246.%E7%B2%BE%E8%AF%BB%E3%80%8APermutation%2C%20Flatten%2C%20Absolute...%E3%80%8B.md">246.精读《Permutation, Flatten, Absolute...》</a>
201201
- <a href="./TS 类型体操/247.%E7%B2%BE%E8%AF%BB%E3%80%8ADiff%2C%20AnyOf%2C%20IsUnion...%E3%80%8B.md">247.精读《Diff, AnyOf, IsUnion...》</a>
202202
- <a href="./TS 类型体操/248.%E7%B2%BE%E8%AF%BB%E3%80%8AMinusOne%2C%20PickByType%2C%20StartsWith...%E3%80%8B.md">248.精读《MinusOne, PickByType, StartsWith...》</a>
203203
- <a href="./TS 类型体操/249.%E7%B2%BE%E8%AF%BB%E3%80%8AObjectEntries%2C%20Shift%2C%20Reverse...%E3%80%8B.md">249.精读《ObjectEntries, Shift, Reverse...》</a>
204+
- <a href="./TS 类型体操/250.%E7%B2%BE%E8%AF%BB%E3%80%8AFlip%2C%20Fibonacci%2C%20AllCombinations...%E3%80%8B.md">250.精读《Flip, Fibonacci, AllCombinations...》</a>
204205

205206
### 设计模式
206207

0 commit comments

Comments
 (0)