|
| 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)) |
0 commit comments