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