Skip to content

Commit a88290a

Browse files
committed
249
1 parent ca4b4d1 commit a88290a

File tree

2 files changed

+308
-1
lines changed

2 files changed

+308
-1
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 41~48 题。
2+
3+
## 精读
4+
5+
### [ObjectEntries](https://github.com/type-challenges/type-challenges/blob/main/questions/02946-medium-objectentries/README.md)
6+
7+
实现 TS 版本的 `Object.entries`
8+
9+
```ts
10+
interface Model {
11+
name: string;
12+
age: number;
13+
locations: string[] | null;
14+
}
15+
type modelEntries = ObjectEntries<Model> // ['name', string] | ['age', number] | ['locations', string[] | null];
16+
```
17+
18+
经过前面的铺垫,大家应该熟悉了 TS 思维思考问题,这道题看到后第一个念头应该是:如何先把对象转换为联合类型?这个问题不解决,就无从下手。
19+
20+
对象或数组转联合类型的思路都是类似的,一个数组转联合类型用 `[number]` 作为下标:
21+
22+
```ts
23+
['1', '2', '3']['number'] // '1' | '2' | '3'
24+
```
25+
26+
对象的方式则是 `[keyof T]` 作为下标:
27+
28+
```ts
29+
type ObjectToUnion<T> = T[keyof T]
30+
```
31+
32+
再观察这道题,联合类型每一项都是数组,分别是 Key 与 Value,这样就比较好写了,我们只要构造一个 Value 是符合结构的对象即可:
33+
34+
```ts
35+
type ObjectEntries<T> = {
36+
[K in keyof T]: [K, T[K]]
37+
}[keyof T]
38+
```
39+
40+
为了通过单测 `ObjectEntries<{ key?: undefined }>`,让 Key 位置不出现 `undefined`,需要强制把对象描述为非可选 Key:
41+
42+
```TS
43+
type ObjectEntries<T> = {
44+
[K in keyof T]-?: [K, T[K]]
45+
}[keyof T]
46+
```
47+
48+
为了通过单测 `ObjectEntries<Partial<Model>>`,得将 Value 中 `undefined` 移除:
49+
50+
```ts
51+
// 本题答案
52+
type RemoveUndefined<T> = [T] extends [undefined] ? T : Exclude<T, undefined>
53+
type ObjectEntries<T> = {
54+
[K in keyof T]-?: [K, RemoveUndefined<T[K]>]
55+
}[keyof T]
56+
```
57+
58+
### [Shift](https://github.com/type-challenges/type-challenges/blob/main/questions/03062-medium-shift/README.md)
59+
60+
实现 TS 版 `Array.shift`
61+
62+
```ts
63+
type Result = Shift<[3, 2, 1]> // [2, 1]
64+
```
65+
66+
这道题应该是简单难度的,只要把第一项抛弃即可,利用 `infer` 轻松实现:
67+
68+
```ts
69+
// 本题答案
70+
type Shift<T> = T extends [infer First, ...infer Rest] ? Rest : never
71+
```
72+
73+
### [Tuple to Nested Object](https://github.com/type-challenges/type-challenges/blob/main/questions/03188-medium-tuple-to-nested-object/README.md)
74+
75+
实现 `TupleToNestedObject<T, P>`,其中 `T` 仅接收字符串数组,`P` 是任意类型,生成一个递归对象结构,满足如下结果:
76+
77+
```ts
78+
type a = TupleToNestedObject<['a'], string> // {a: string}
79+
type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}}
80+
type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type
81+
```
82+
83+
这道题用到了 5 个知识点:递归、辅助类型、`infer`、如何指定对象 Key、`PropertyKey`,你得全部知道并组合起来才能解决该题。
84+
85+
首先因为返回值是个递归对象,递归过程中必定不断修改它,因此给泛型添加第三个参数 `R` 存储这个对象,并且在递归数组时从最后一个开始,这样从最内层对象开始一点点把它 “包起来”:
86+
87+
```ts
88+
type TupleToNestedObject<T, U, R = U> = /** 伪代码
89+
T extends [...infer Rest, infer Last]
90+
*/
91+
```
92+
93+
下一步是如何描述一个对象 Key?之前 `Chainable Options` 例子我们学到的 `K in Q`,但需要注意直接这么写会报错,因为必须申明 `Q extends PropertyKey`。最后再处理一下递归结束条件,即 `T` 变成空数组时直接返回 `R`
94+
95+
```ts
96+
// 本题答案
97+
type TupleToNestedObject<T, U, R = U> = T extends [] ? R : (
98+
T extends [...infer Rest, infer Last extends PropertyKey] ? (
99+
TupleToNestedObject<Rest, U, {
100+
[P in Last]: R
101+
}>
102+
) : never
103+
)
104+
```
105+
106+
### [Reverse](https://github.com/type-challenges/type-challenges/blob/main/questions/03192-medium-reverse/README.md)
107+
108+
实现 TS 版 `Array.reverse`
109+
110+
```ts
111+
type a = Reverse<['a', 'b']> // ['b', 'a']
112+
type b = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a']
113+
```
114+
115+
这道题比上一题简单,只需要用一个递归即可:
116+
117+
```ts
118+
// 本题答案
119+
type Reverse<T extends any[]> = T extends [...infer Rest, infer End] ? [End, ...Reverse<Rest>] : T
120+
```
121+
122+
### [Flip Arguments](https://github.com/type-challenges/type-challenges/blob/main/questions/03196-medium-flip-arguments/README.md)
123+
124+
实现 `FlipArguments<T>` 将函数 `T` 的参数反转:
125+
126+
```ts
127+
type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>
128+
// (arg0: boolean, arg1: number, arg2: string) => void
129+
```
130+
131+
本题与上题类似,只是反转内容从数组变成了函数的参数,只要用 `infer` 定义出函数的参数,利用 `Reverse` 函数反转一下即可:
132+
133+
```ts
134+
// 本题答案
135+
type Reverse<T extends any[]> = T extends [...infer Rest, infer End] ? [End, ...Reverse<Rest>] : T
136+
137+
type FlipArguments<T> =
138+
T extends (...args: infer Args) => infer Result ? (...args: Reverse<Args>) => Result : never
139+
```
140+
141+
### [FlattenDepth](https://github.com/type-challenges/type-challenges/blob/main/questions/03243-medium-flattendepth/README.md)
142+
143+
实现指定深度的 Flatten:
144+
145+
```ts
146+
type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2> // [1, 2, 3, 4, [5]]. flattern 2 times
147+
type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, [[5]]]. Depth defaults to be 1
148+
```
149+
150+
这道题比之前的 `Flatten` 更棘手一些,因为需要控制打平的次数。
151+
152+
基本想法就是,打平 `Deep` 次,所以需要实现打平一次的函数,再根据 `Deep` 值递归对应次:
153+
154+
```ts
155+
type FlattenOnce<T extends any[], U extends any[] = []> = T extends [infer X, ...infer Y] ? (
156+
X extends any[] ? FlattenOnce<Y, [...U, ...X]> : FlattenOnce<Y, [...U, X]>
157+
) : U
158+
```
159+
160+
然后再实现主函数 `FlattenDepth`,因为 TS 无法实现 +、- 号运算,我们必须用数组长度判断与操作数组来辅助实现:
161+
162+
```ts
163+
// FlattenOnce
164+
type FlattenDepth<
165+
T extends any[],
166+
U extends number = 1,
167+
P extends any[] = []
168+
> = P['length'] extends U ? T : (
169+
FlattenDepth<FlattenOnce<T>, U, [...P, any]>
170+
)
171+
```
172+
173+
当递归没有达到深度 `U` 时,就用 `[...P, any]` 的方式给数组塞一个元素,下次如果能匹配上 `P['length'] extends U` 说明递归深度已达到。
174+
175+
但考虑到测试用例 `FlattenDepth<[1, [2, [3, [4, [5]]]]], 19260817>` 会引发超长次数递归,需要提前终止,即如果打平后已经是平的,就不用再继续递归了,此时可以用 `FlattenOnce<T> extends T` 判断:
176+
177+
```ts
178+
// 本题答案
179+
// FlattenOnce
180+
type FlattenDepth<
181+
T extends any[],
182+
U extends number = 1,
183+
P extends any[] = []
184+
> = P['length'] extends U ? T : (
185+
FlattenOnce<T> extends T ? T : (
186+
FlattenDepth<FlattenOnce<T>, U, [...P, any]>
187+
)
188+
)
189+
```
190+
191+
### [BEM style string](https://github.com/type-challenges/type-challenges/blob/main/questions/03326-medium-bem-style-string/README.md)
192+
193+
实现 `BEM` 函数完成其规则拼接:
194+
195+
```ts
196+
Expect<Equal<BEM<'btn', [], ['small', 'medium', 'large']>, 'btn--small' | 'btn--medium' | 'btn--large' >>,
197+
```
198+
199+
之前我们了解了通过下标将数组或对象转成联合类型,这里还有一个特殊情况,即字符串中通过这种方式申明每一项,会自动笛卡尔积为新的联合类型:
200+
201+
```ts
202+
type BEM<B extends string, E extends string[], M extends string[]> =
203+
`${B}__${E[number]}--${M[number]}`
204+
```
205+
206+
这是最简单的写法,但没有考虑项不存在的情况。不如创建一个 `SafeUnion` 函数,当传入值不存在时返回空字符串,保证安全的跳过:
207+
208+
```ts
209+
type IsNever<TValue> = TValue[] extends never[] ? true : false;
210+
type SafeUnion<TUnion> = IsNever<TUnion> extends true ? "" : TUnion;
211+
```
212+
213+
最终代码:
214+
215+
```ts
216+
// 本题答案
217+
// IsNever, SafeUnion
218+
type BEM<B extends string, E extends string[], M extends string[]> =
219+
`${B}${SafeUnion<`__${E[number]}`>}${SafeUnion<`--${M[number]}`>}`
220+
```
221+
222+
### [InorderTraversal](https://github.com/type-challenges/type-challenges/blob/main/questions/03376-medium-inordertraversal/README.md)
223+
224+
实现 TS 版二叉树中序遍历:
225+
226+
```ts
227+
const tree1 = {
228+
val: 1,
229+
left: null,
230+
right: {
231+
val: 2,
232+
left: {
233+
val: 3,
234+
left: null,
235+
right: null,
236+
},
237+
right: null,
238+
},
239+
} as const
240+
241+
type A = InorderTraversal<typeof tree1> // [1, 3, 2]
242+
```
243+
244+
首先回忆一下二叉树中序遍历 JS 版的实现:
245+
246+
```js
247+
function inorderTraversal(tree) {
248+
if (!tree) return []
249+
return [
250+
...inorderTraversal(tree.left),
251+
res.push(val),
252+
...inorderTraversal(tree.right)
253+
]
254+
}
255+
```
256+
257+
对 TS 来说,实现递归的方式有一点点不同,即通过 `extends TreeNode` 来判定它不是 Null 从而递归:
258+
259+
```ts
260+
// 本题答案
261+
interface TreeNode {
262+
val: number
263+
left: TreeNode | null
264+
right: TreeNode | null
265+
}
266+
type InorderTraversal<T extends TreeNode | null> = [T] extends [TreeNode] ? (
267+
[
268+
...InorderTraversal<T['left']>,
269+
T['val'],
270+
...InorderTraversal<T['right']>
271+
]
272+
): []
273+
```
274+
275+
你可能会问,问什么不能像 JS 一样,用 `null` 做判断呢?
276+
277+
```ts
278+
type InorderTraversal<T extends TreeNode | null> = [T] extends [null] ? [] : (
279+
[ // error
280+
...InorderTraversal<T['left']>,
281+
T['val'],
282+
...InorderTraversal<T['right']>
283+
]
284+
)
285+
```
286+
287+
如果这么写会发现 TS 抛出了异常,因为 TS 不能确定 `T` 此时符合 `TreeNode` 类型,所以要执行操作时一般采用正向判断。
288+
289+
## 总结
290+
291+
这些类型挑战题目需要灵活组合 TS 的基础知识点才能破解,常用的包括:
292+
293+
- 如何操作对象,增减 Key、只读、合并为一个对象等。
294+
- 递归,以及辅助类型。
295+
- `infer` 知识点。
296+
- 联合类型,如何从对象或数组生成联合类型,字符串模板与联合类型的关系。
297+
298+
> 讨论地址是:[精读《ObjectEntries, Shift, Reverse...》· Issue #431 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/431)
299+
300+
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
301+
302+
> 关注 **前端精读微信公众号**
303+
304+
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
305+
306+
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))

readme.md

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

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

9-
最新精读:<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>
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>
1010

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

@@ -200,6 +200,7 @@
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>
203+
- <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>
203204

204205
### 设计模式
205206

0 commit comments

Comments
 (0)