解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 63~68 题。
精读
Unique
实现 Unique<T>,对 T 去重:
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 逐一拆解,判断第一个字符是否在结果数组里,如果不在就塞进去:
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
每次取首项,如果等于 Value 直接返回 true,否则继续递归,如果数组递归结束(不构成 Arr extends [xxx] 的形式)说明递归完了还没有找到相等值,直接返回 false。
把这两个函数组合一下就能轻松解决本题:
// 本题答案
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 的描述来替换类型:
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 的作用,我们包一层数组进行判断:
type MapTypes<T, R extends { mapFrom: any; mapTo: any }> = {
[K in keyof T]: [T[K]] extends [R['mapFrom']] ? R['mapTo'] : T[K]
}
但这个解答还有一个 case 无法通过:
MapTypes<{iWillBeNumberOrDate: string}, StringToDate | StringToNumber> // gives { iWillBeNumberOrDate: number | Date; }
我们需要考虑到 Union 分发机制以及每次都要重新匹配一次是否命中 mapFrom,因此需要抽一个函数:
type Transform<R extends { mapFrom: any; mapTo: any }, T> = R extends any
? T extends R['mapFrom']
? R['mapTo']
: never
: never
为什么要 R extends any 看似无意义的写法呢?原因是 R 是联合类型,这样可以触发分发机制,让每一个类型独立判断。所以最终答案就是:
// 本题答案
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:
type result = ConstructTuple<2> // expect to be [unknown, unkonwn]
比较容易想到的办法是利用下标递归:
type ConstructTuple<
L extends number,
I extends number[] = []
> = I['length'] extends L ? [] : [unknown, ...ConstructTuple<L, [1, ...I]>]
但在如下测试用例会遇到递归长度过深的问题:
ConstructTuple<999> // Type instantiation is excessively deep and possibly infinite
一种解法是利用 minusOne 提到的 CountTo 方法快捷生成指定长度数组,把 1 替换为 unknown 即可:
// 本题答案
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>,生成数字为从 T 到 P 的联合类型:
type result = NumberRange<2, 9> // | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
以 NumberRange<2, 9> 为例,我们需要实现 2 到 9 的递增递归,因此需要一个数组长度从 2 递增到 9 的辅助变量 U,以及一个存储结果的辅助变量 R:
type NumberRange<T, P, U extends any[] = 长度为 T 的数组, R>
所以我们先实现 LengthTo 函数,传入长度 N,返回一个长度为 N 的数组:
type LengthTo<N extends number, R extends any[] = []> =
R['length'] extends N ? R : LengthTo<N, [0, ...R]>
然后就是递归了:
// 本题答案
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>:
// 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 类似:
type AllCombinations_ABC = AllCombinations<'ABC'>
// should be '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA'
还记得这题吗?我们要将字符串变成联合类型:
type StrToUnion<S> = S extends `${infer F}${infer R}`
? F | StrToUnion<R>
: never
而本题 Combination 更简单,把数组转换为联合类型只需要 T[number]。所以本题第一种组合解法是,将 AllCombinations 稍微改造下,再利用 Exclude 和 TrimRight 删除多余的空格:
// 本题答案
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>, ''>>
还有一种非常精彩的答案在此分析一下:
// 本题答案
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> 输出所有可能的子序列:
type A = Subsequence<[1, 2]> // [] | [1] | [2] | [1, 2]
因为是返回数组的全排列,只要每次取第一项,与剩余项的递归构造出结果,| 上剩余项本身递归的结果就可以了:
// 本题答案
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 许可证)