当前期刊数: 252

解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 63~68 题。

精读

Unique

实现 Unique<T>,对 T 去重:

1
2
3
4
5
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 配合,并把 Tinfer 逐一拆解,判断第一个字符是否在结果数组里,如果不在就塞进去:

1
2
3
4
5
type Unique<T, R extends any[] = []> = T extends [infer F, ...infer Rest]
? Includes<R, F> extends true
? Unique<Rest, R>
: Unique<Rest, [...R, F]>
: R

那么剩下的问题就是,如何判断一个对象是否出现在数组中,使用递归可以轻松完成:

1
2
3
4
5
type Includes<Arr, Value> = Arr extends [infer F, ...infer Rest]
? Equal<F, Value> extends true
? true
: Includes<Rest, Value>
: false

每次取首项,如果等于 Value 直接返回 true,否则继续递归,如果数组递归结束(不构成 Arr extends [xxx] 的形式)说明递归完了还没有找到相等值,直接返回 false

把这两个函数组合一下就能轻松解决本题:

1
2
3
4
5
6
7
8
9
10
11
12
// 本题答案
type Unique<T, R extends any[] = []> = T extends [infer F, ...infer Rest]
? Includes<R, F> extends true
? Unique<Rest, R>
: Unique<Rest, [...R, F]>
: R

type Includes<Arr, Value> = Arr extends [infer F, ...infer Rest]
? Equal<F, Value> extends true
? true
: Includes<Rest, Value>
: false

MapTypes

实现 MapTypes<T, R>,根据对象 R 的描述来替换类型:

1
2
3
4
5
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 的作用,我们包一层数组进行判断:

1
2
3
type MapTypes<T, R extends { mapFrom: any; mapTo: any }> = {
[K in keyof T]: [T[K]] extends [R['mapFrom']] ? R['mapTo'] : T[K]
}

但这个解答还有一个 case 无法通过:

1
MapTypes<{iWillBeNumberOrDate: string}, StringToDate | StringToNumber> // gives { iWillBeNumberOrDate: number | Date; }

我们需要考虑到 Union 分发机制以及每次都要重新匹配一次是否命中 mapFrom,因此需要抽一个函数:

1
2
3
4
5
type Transform<R extends { mapFrom: any; mapTo: any }, T> = R extends any
? T extends R['mapFrom']
? R['mapTo']
: never
: never

为什么要 R extends any 看似无意义的写法呢?原因是 R 是联合类型,这样可以触发分发机制,让每一个类型独立判断。所以最终答案就是:

1
2
3
4
5
6
7
8
9
10
// 本题答案
type MapTypes<T, R extends { mapFrom: any; mapTo: any }> = {
[K in keyof T]: [T[K]] extends [R['mapFrom']] ? Transform<R, T[K]> : T[K]
}

type Transform<R extends { mapFrom: any; mapTo: any }, T> = R extends any
? T extends R['mapFrom']
? R['mapTo']
: never
: never

Construct Tuple

生成指定长度的 Tuple:

1
type result = ConstructTuple<2> // expect to be [unknown, unkonwn]

比较容易想到的办法是利用下标递归:

1
2
3
4
type ConstructTuple<
L extends number,
I extends number[] = []
> = I['length'] extends L ? [] : [unknown, ...ConstructTuple<L, [1, ...I]>]

但在如下测试用例会遇到递归长度过深的问题:

1
ConstructTuple<999> // Type instantiation is excessively deep and possibly infinite

一种解法是利用 minusOne 提到的 CountTo 方法快捷生成指定长度数组,把 1 替换为 unknown 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// 本题答案
type ConstructTuple<L extends number> = CountTo<`${L}`>

type CountTo<
T extends string,
Count extends unknown[] = []
> = T extends `${infer First}${infer Rest}`
? CountTo<Rest, N<Count>[keyof N & First]>
: Count

type N<T extends unknown[] = []> = {
'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

实现 NumberRange<T, P>,生成数字为从 TP 的联合类型:

1
type result = NumberRange<2, 9> //  | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 

NumberRange<2, 9> 为例,我们需要实现 29 的递增递归,因此需要一个数组长度从 2 递增到 9 的辅助变量 U,以及一个存储结果的辅助变量 R:

1
type NumberRange<T, P, U extends any[] = 长度为 T 的数组, R>

所以我们先实现 LengthTo 函数,传入长度 N,返回一个长度为 N 的数组:

1
2
type LengthTo<N extends number, R extends any[] = []> =
R['length'] extends N ? R : LengthTo<N, [0, ...R]>

然后就是递归了:

1
2
3
4
5
6
7
// 本题答案
type NumberRange<T extends number, P extends number, U extends any[] = LengthTo<T>, R extends number = never> =
U['length'] extends P ? (
R | U['length']
) : (
NumberRange<T, P, [0, ...U], R | U['length']>
)

R 的默认值为 never 非常重要,否则默认值为 any,最终类型就会被放大为 any

Combination

实现 Combination<T>:

1
2
// 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 类似:

1
2
type AllCombinations_ABC = AllCombinations<'ABC'>
// should be '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA'

还记得这题吗?我们要将字符串变成联合类型:

1
2
3
type StrToUnion<S> = S extends `${infer F}${infer R}`
? F | StrToUnion<R>
: never

而本题 Combination 更简单,把数组转换为联合类型只需要 T[number]。所以本题第一种组合解法是,将 AllCombinations 稍微改造下,再利用 ExcludeTrimRight 删除多余的空格:

1
2
3
4
5
6
7
8
9
10
// 本题答案
type AllCombinations<T extends string[], U extends string = T[number]> = [
U
] extends [never]
? ''
: '' | { [K in U]: `${K} ${AllCombinations<never, Exclude<U, K>>}` }[U]

type TrimRight<T extends string> = T extends `${infer R} ` ? TrimRight<R> : T

type Combination<T extends string[]> = TrimRight<Exclude<AllCombinations<T>, ''>>

还有一种非常精彩的答案在此分析一下:

1
2
3
4
// 本题答案
type Combination<T extends string[], U = T[number], A = U> = U extends infer U extends string
? `${U} ${Combination<T, Exclude<A, U>>}` | U
: never;

依然利用 T[number] 的特性将数组转成联合类型,再利用联合类型 extends 会分组的特性递归出结果。

之所以不会出现结尾出现多余的空格,是因为 U extends infer U extends string 这段判断已经杜绝了 U 消耗完的情况,如果消耗完会及时返回 never,所以无需用 TrimRight 处理右侧多余的空格。

至于为什么要定义 A = U,在前面章节已经介绍过了,因为联合类型 extends 过程中会进行分组,此时访问的 U 已经是具体类型了,但此时访问 A 还是原始的联合类型 U

Subsequence

实现 Subsequence<T> 输出所有可能的子序列:

1
type A = Subsequence<[1, 2]> // [] | [1] | [2] | [1, 2]

因为是返回数组的全排列,只要每次取第一项,与剩余项的递归构造出结果,| 上剩余项本身递归的结果就可以了:

1
2
3
4
// 本题答案
type Subsequence<T extends number[]> = T extends [infer F, ...infer R extends number[]] ? (
Subsequence<R> | [F, ...Subsequence<R>]
) : T

总结

对全排列问题有两种经典解法:

  • 利用辅助变量方式递归,注意联合类型与字符串、数组之间转换的技巧。
  • 直接递归,不借助辅助变量,一般在题目返回类型容易构造时选择。

讨论地址是:精读《Unique, MapTypes, Construct Tuple…》· Issue ##434 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证


本站由 钟意 使用 Stellar 1.28.1 主题创建。
又拍云 提供CDN加速/云存储服务
vercel 提供托管服务
湘ICP备2023019799号-1
总访问 次 | 本页访问