我正在尝试定义一个通用的 zip 函数:
type ZippedElement<T extends unknown[][]> = {
[K in keyof T]: T[K] extends (infer V)[] ? V : never;
};
export function zip<T extends unknown[][]>(...args: T) {
const minLength = Math.min(...args.map((arr) => arr.length));
return Array.from({ length: minLength }, (_, i) =>
args.map((arr) => arr[i]),
) as ZippedElement<T>[];
}
它似乎适用于动态数组:
const a: number[] = ...;
const b: string[] = ...;
zip(a, b) // ==> [number, string][]
但我希望我的类型检查也能在可能的情况下推断元组,所以我也希望它能够工作:
const a: [number, number] = [1, 2];
const b: [string, string] = ["a", "b"];
zip(a, b) // ==> [[number, string], [number, string]]
我为自己创建了一个实用程序,用于创建特定大小的元组:
type StaticArray<L extends number, T, R extends any[] = T[]> =
R extends { length: L }
? R
: StaticArray<L, T, [...R, T]>;
所缺少的是一种类型,它采用
T extends unknown[][]
并返回所有元组的最小大小的元组(如果所有参数都是元组),并返回一个数组(如果输入中有一个数组):
type ZippedTupleOrArray<T extends unknown[][]> =
T extends /* magic infer minimum tuple size N */
? StaticArray<N, ZippedElement<T>>
: Array<ZippedElement<T>>;
真的可以实现吗?如果是的话,怎么办?
有多种方式来编写
MinimumLength<T>
,这样,当 T
是固定长度元组的固定长度 tuple 时,它的计算结果为最短的此类元组的 length
属性。 这个答案中提出了一种这样的方法。
当然,这种类型杂耍往往会出现奇怪的边缘情况行为,因此针对各种用例测试任何潜在的解决方案以确保它满足您的需求非常重要。
您似乎关心的情况是检测是否有任何成员只是数组类型而不是元组。
在这个答案中我不关心的边缘情况:
如果我们有一个适用于固定长度元组的
MimimumLength<T>
,您可以编写
ZippedTupleOrArray
来处理非元组类型,只需检查元素并集的length
属性是否为number
即可。 像 [0, 0]
这样的固定长度元组对于 length
具有数字 文字类型,例如 2
。 另一方面,像 string[]
这样的常规数组类型具有 number
。 任何数字文字类型与 number
的并集就是 number
。因此,如果 T
的任何元素不是元组,那么 T[number]['length']
将是 number
。否则它将是 number
:的某个适当的子类型
type ZippedTupleOrArray<T extends unknown[][]> =
number extends T[number]['length'] ?
Array<ZippedElement<T>> : StaticArray<MinimumLength<T>, ZippedElement<T>>
现在我们可以实现
MinimumLength<T>
并假设
T
中的所有元组:type MinimumLength<T extends any[][], N extends number = number> =
T extends [infer F extends any[], ...infer R extends any[][]] ?
MinimumLength<R, `${N}` extends keyof F ? N : F['length']> :
N
其工作方式是识别元组具有与索引对应的类似数字的字符串文字键。所以
keyof ["", "", ""]
将包括
"0"
、"1"
和 "2"
。 如果您采用 length
并使用 模板文字类型对其进行字符串化,您将得到一个比所有这些都大 1 的类似数字的字符串文字类型:所以
`${["", "", ""]['length']}`
是 "3"
。因此我们可以遍历T
并累积最小长度
N
(使用尾递归条件类型)。对于
F
的每个元素 T
,我们检查字符串化的 N
是否是 F
的键。 如果是,那么 F
比 N
长,所以我们保持 N
原样。 如果不是,那么 F
比 N
短,所以我们扔掉 N
并使用 F['length']
代替。我们从 N
的
number
开始,这样 MinimumLength<[]>
就是 number
。一旦我们检查类似 MinimumLength<[["","",""]]>
的内容,那么 number
首先会被替换为第一个元素的长度,因为 `${number}`
并不是明确的固定长度元组的键。当我们遍历完所有 T
并最终得到
MinimumLength<[], N>
后,我们返回累积的 N
。
让我们测试一下:type Test0 = MinimumLength<[[1], [2, 2], [], [3, 3, 3], [2, 2]]>;
// ^? type Test0 = 0
type Test1 = MinimumLength<[[1], [2, 2], [3, 3, 3]]>;
// ^? type Test1 = 1
看起来不错。现在让我们测试一下
zip()
declare function zip<T extends unknown[][]>(...args: T): ZippedTupleOrArray<T>;
declare const a: number[];
declare const b: string[];
zip(a, b) // [number, string][]
const c: [number, number] = [1, 2];
const d: [string, string] = ["a", "b"];
zip(c, d) // [[number, string], [number, string]]
zip(a, b, c, d); // [number, string, number, string][]
zip(c, d, [null, null, null, null] as const);
// [[number, string, null], [number, string, null]]
看起来也不错。Playground 代码链接