diff --git "a/TS \347\261\273\345\236\213\344\275\223\346\223\215/252.\347\262\276\350\257\273\343\200\212Unique, MapTypes, Construct Tuple...\343\200\213.md" "b/TS \347\261\273\345\236\213\344\275\223\346\223\215/252.\347\262\276\350\257\273\343\200\212Unique, MapTypes, Construct Tuple...\343\200\213.md" new file mode 100644 index 00000000..df788298 --- /dev/null +++ "b/TS \347\261\273\345\236\213\344\275\223\346\223\215/252.\347\262\276\350\257\273\343\200\212Unique, MapTypes, Construct Tuple...\343\200\213.md" @@ -0,0 +1,410 @@ +解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 63~68 题。 + +## 精读 + +### [Unique](https://github.com/type-challenges/type-challenges/blob/main/questions/05360-medium-unique/README.md) + +实现 `Unique`,对 `T` 去重: + +```ts +type Res = Unique<[1, 1, 2, 2, 3, 3]> // expected to be [1, 2, 3] +type Res1 = Unique<[1, 2, 3, 4, 4, 5, 6, 7]> // expected to be [1, 2, 3, 4, 5, 6, 7] +type Res2 = Unique<[1, 'a', 2, 'b', 2, 'a']> // expected to be [1, "a", 2, "b"] +type Res3 = Unique<[string, number, 1, 'a', 1, string, 2, 'b', 2, number]> // expected to be [string, number, 1, "a", 2, "b"] +type Res4 = Unique<[unknown, unknown, any, any, never, never]> // expected to be [unknown, any, never] +``` + +去重需要不断递归产生去重后结果,因此需要一个辅助变量 `R` 配合,并把 `T` 用 `infer` 逐一拆解,判断第一个字符是否在结果数组里,如果不在就塞进去: + +```ts +type Unique = T extends [infer F, ...infer Rest] + ? Includes extends true + ? Unique + : Unique + : R +``` + +那么剩下的问题就是,如何判断一个对象是否出现在数组中,使用递归可以轻松完成: + +```ts +type Includes = Arr extends [infer F, ...infer Rest] + ? Equal extends true + ? true + : Includes + : false +``` + +每次取首项,如果等于 `Value` 直接返回 `true`,否则继续递归,如果数组递归结束(不构成 `Arr extends [xxx]` 的形式)说明递归完了还没有找到相等值,直接返回 `false`。 + +把这两个函数组合一下就能轻松解决本题: + +```ts +// 本题答案 +type Unique = T extends [infer F, ...infer Rest] + ? Includes extends true + ? Unique + : Unique + : R + +type Includes = Arr extends [infer F, ...infer Rest] + ? Equal extends true + ? true + : Includes + : false +``` + +### [MapTypes](https://github.com/type-challenges/type-challenges/blob/main/questions/05821-medium-maptypes/README.md) + +实现 `MapTypes`,根据对象 `R` 的描述来替换类型: + +```ts +type StringToNumber = { + mapFrom: string; // value of key which value is string + mapTo: number; // will be transformed for number +} +MapTypes<{iWillBeANumberOneDay: string}, StringToNumber> // gives { iWillBeANumberOneDay: number; } +``` + +因为要返回一个新对象,所以我们使用 `{ [K in keyof T]: ... }` 的形式描述结果对象。然后就要对 Value 类型进行判断了,为了防止 `never` 的作用,我们包一层数组进行判断: + +```ts +type MapTypes = { + [K in keyof T]: [T[K]] extends [R['mapFrom']] ? R['mapTo'] : T[K] +} +``` + +但这个解答还有一个 case 无法通过: + +```ts +MapTypes<{iWillBeNumberOrDate: string}, StringToDate | StringToNumber> // gives { iWillBeNumberOrDate: number | Date; } +``` + +我们需要考虑到 Union 分发机制以及每次都要重新匹配一次是否命中 `mapFrom`,因此需要抽一个函数: + +```ts +type Transform = R extends any + ? T extends R['mapFrom'] + ? R['mapTo'] + : never + : never +``` + +为什么要 `R extends any` 看似无意义的写法呢?原因是 `R` 是联合类型,这样可以触发分发机制,让每一个类型独立判断。所以最终答案就是: + +```ts +// 本题答案 +type MapTypes = { + [K in keyof T]: [T[K]] extends [R['mapFrom']] ? Transform : T[K] +} + +type Transform = R extends any + ? T extends R['mapFrom'] + ? R['mapTo'] + : never + : never +``` + +### [Construct Tuple](https://github.com/type-challenges/type-challenges/blob/main/questions/07544-medium-construct-tuple/README.md) + +生成指定长度的 Tuple: + +```ts +type result = ConstructTuple<2> // expect to be [unknown, unkonwn] +``` + +比较容易想到的办法是利用下标递归: + +```ts +type ConstructTuple< + L extends number, + I extends number[] = [] +> = I['length'] extends L ? [] : [unknown, ...ConstructTuple] +``` + +但在如下测试用例会遇到递归长度过深的问题: + +```ts +ConstructTuple<999> // Type instantiation is excessively deep and possibly infinite +``` + +一种解法是利用 [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#minusone) 提到的 `CountTo` 方法快捷生成指定长度数组,把 `1` 替换为 `unknown` 即可: + +```ts +// 本题答案 +type ConstructTuple = CountTo<`${L}`> + +type CountTo< + T extends string, + Count extends unknown[] = [] +> = T extends `${infer First}${infer Rest}` + ? CountTo[keyof N & First]> + : Count + +type N = { + '0': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T] + '1': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, unknown] + '2': [ + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + unknown, + unknown + ] + '3': [ + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + unknown, + unknown, + unknown + ] + '4': [ + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + unknown, + unknown, + unknown, + unknown + ] + '5': [ + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + unknown, + unknown, + unknown, + unknown, + unknown + ] + '6': [ + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown + ] + '7': [ + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown + ] + '8': [ + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown + ] + '9': [ + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + ...T, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown + ] +} +``` + +### [Number Range](https://github.com/type-challenges/type-challenges/blob/main/questions/08640-medium-number-range/README.md) + +实现 `NumberRange`,生成数字为从 `T` 到 `P` 的联合类型: + +```ts +type result = NumberRange<2, 9> // | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 +``` + +以 `NumberRange<2, 9>` 为例,我们需要实现 `2` 到 `9` 的递增递归,因此需要一个数组长度从 `2` 递增到 `9` 的辅助变量 `U`,以及一个存储结果的辅助变量 `R`: + +```ts +type NumberRange +``` + +所以我们先实现 `LengthTo` 函数,传入长度 `N`,返回一个长度为 `N` 的数组: + +```ts +type LengthTo = + R['length'] extends N ? R : LengthTo +``` + +然后就是递归了: + +```ts +// 本题答案 +type NumberRange, R extends number = never> = + U['length'] extends P ? ( + R | U['length'] + ) : ( + NumberRange + ) +``` + +`R` 的默认值为 `never` 非常重要,否则默认值为 `any`,最终类型就会被放大为 `any`。 + +### [Combination](https://github.com/type-challenges/type-challenges/blob/main/questions/08767-medium-combination/README.md) + +实现 `Combination`: + +```ts +// expected to be `"foo" | "bar" | "baz" | "foo bar" | "foo bar baz" | "foo baz" | "foo baz bar" | "bar foo" | "bar foo baz" | "bar baz" | "bar baz foo" | "baz foo" | "baz foo bar" | "baz bar" | "baz bar foo"` +type Keys = Combination<['foo', 'bar', 'baz']> +``` + +本题和 `AllCombination` 类似: + +```ts +type AllCombinations_ABC = AllCombinations<'ABC'> +// should be '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA' +``` + +还记得这题吗?我们要将字符串变成联合类型: + +```ts +type StrToUnion = S extends `${infer F}${infer R}` + ? F | StrToUnion + : never +``` + +而本题 `Combination` 更简单,把数组转换为联合类型只需要 `T[number]`。所以本题第一种组合解法是,将 `AllCombinations` 稍微改造下,再利用 `Exclude` 和 `TrimRight` 删除多余的空格: + +```ts +// 本题答案 +type AllCombinations = [ + U +] extends [never] + ? '' + : '' | { [K in U]: `${K} ${AllCombinations>}` }[U] + +type TrimRight = T extends `${infer R} ` ? TrimRight : T + +type Combination = TrimRight, ''>> +``` + +还有一种非常精彩的答案在此分析一下: + +```ts +// 本题答案 +type Combination = U extends infer U extends string + ? `${U} ${Combination>}` | U + : never; +``` + +依然利用 `T[number]` 的特性将数组转成联合类型,再利用联合类型 `extends` 会分组的特性递归出结果。 + +之所以不会出现结尾出现多余的空格,是因为 `U extends infer U extends string` 这段判断已经杜绝了 `U` 消耗完的情况,如果消耗完会及时返回 `never`,所以无需用 `TrimRight` 处理右侧多余的空格。 + +至于为什么要定义 `A = U`,在前面章节已经介绍过了,因为联合类型 `extends` 过程中会进行分组,此时访问的 `U` 已经是具体类型了,但此时访问 `A` 还是原始的联合类型 `U`。 + +### [Subsequence](https://github.com/type-challenges/type-challenges/blob/main/questions/08987-medium-subsequence/README.md) + +实现 `Subsequence` 输出所有可能的子序列: + +```ts +type A = Subsequence<[1, 2]> // [] | [1] | [2] | [1, 2] +``` + +因为是返回数组的全排列,只要每次取第一项,与剩余项的递归构造出结果,`|` 上剩余项本身递归的结果就可以了: + +```ts +// 本题答案 +type Subsequence = T extends [infer F, ...infer R extends number[]] ? ( + Subsequence | [F, ...Subsequence] +) : T +``` + +## 总结 + +对全排列问题有两种经典解法: + +- 利用辅助变量方式递归,注意联合类型与字符串、数组之间转换的技巧。 +- 直接递归,不借助辅助变量,一般在题目返回类型容易构造时选择。 + +> 讨论地址是:[精读《Unique, MapTypes, Construct Tuple...》· Issue #434 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/434) + +**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** + +> 关注 **前端精读微信公众号** + + + +> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) diff --git a/readme.md b/readme.md index 97269f93..d64a2e11 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ 前端界的好文精读,每周更新! -最新精读:251.精读《Trim Right, Without, Trunc...》 +最新精读:252.精读《Unique, MapTypes, Construct Tuple...》 素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2) @@ -203,6 +203,7 @@ - 249.精读《ObjectEntries, Shift, Reverse...》 - 250.精读《Flip, Fibonacci, AllCombinations...》 - 251.精读《Trim Right, Without, Trunc...》 +- 252.精读《Unique, MapTypes, Construct Tuple...》 ### 设计模式