打字稿中的通用 zip,使用 variadc 元组
Generic zip in typescript, using variadc tuple
我有 python 背景,我喜欢使用函数式语言,而且我正在学习打字稿,所以我想要一个 zip()
函数,它的工作方式有点像 Python的。我看到打字稿已经获得了生成器和可变泛型,所以我认为这应该是可能的。我有一些东西 非常 接近我想要的东西,但还不完全是。我确定我要么在做一些显而易见的事情,要么我即将学习 typescript 类型系统的深层工作原理。
我想要的是一个函数,它接受可变数量的数组,并从这些数组中生成一个元素元组,直到最短的数组用完为止。我写了一个生成器来执行此操作,但我无法正确输入。
function* zip<T extends any[]>(...args: T) {
for (let i = 0; i < Math.min(...args.map((e) => { return e.length })); ++i) {
yield args.map((e) => { return e[i]; }) as T;
}
}
如果我删除 as T
那么该函数可以工作,但我会丢失所有类型信息,如果没有,我会得到 A return 类型 [...T[]]
,但我想要 [...T]
.
例如zip([1, 2, 3], ['a', 'b', 'c'])
,应该有return类型的[number, string]
,但它有[number[], string[]]
您可能希望打字看起来像这样:
function* zip<T extends any[][]>(...args: T) {
for (let i = 0; i < Math.min(...args.map((e) => { return e.length })); ++i) {
yield args.map((e) => { return e[i]; }) as
{ [I in keyof T]: T[I] extends Array<infer E> ? E : never };
}
}
请注意,您希望 args
成为 个数组 的数组,对吗? args
的每个元素本身都应该有一个 length
属性 并且具有可通过数字索引访问的元素。所以 T extends any[][]
会为你做这件事(你甚至可能想放宽它以便它也接受 readonly
arrays,但我不会深入讨论。)
您不想 return T
生成器生成的每个元素,因为这是一个数组数组。如果你不注释 return 那么你会得到编译器认为 map()
产生的任何东西,这必然会比你正在寻找的更不精确(参见 for more information). You have to assert 类型, 但它不是 T
.
相反,对于 T
的索引中的每个数字索引 I
,您希望在该索引处采用数组类型 T[I]
并且 return 该数组的元素类型。我们可以使用 mapped type to represent this transformation, because mapped types on tuples produce tuples。这就是 { [I in keyof T]: T[I] extends Array<infer E> ? E : never }
所做的。
好的,我们来测试一下:
for (const z of zip([1, 2, 3], ['a', 'b', 'c'])) {
console.log(z[0].toFixed(2) + ", " + z[1].toUpperCase())
// "1.00, A" "2.00, B" "3.00, C"
}
看起来不错;编译器认为 z
是 [number, string]
并让您相应地处理 z
的每个元素。
我有 python 背景,我喜欢使用函数式语言,而且我正在学习打字稿,所以我想要一个 zip()
函数,它的工作方式有点像 Python的。我看到打字稿已经获得了生成器和可变泛型,所以我认为这应该是可能的。我有一些东西 非常 接近我想要的东西,但还不完全是。我确定我要么在做一些显而易见的事情,要么我即将学习 typescript 类型系统的深层工作原理。
我想要的是一个函数,它接受可变数量的数组,并从这些数组中生成一个元素元组,直到最短的数组用完为止。我写了一个生成器来执行此操作,但我无法正确输入。
function* zip<T extends any[]>(...args: T) {
for (let i = 0; i < Math.min(...args.map((e) => { return e.length })); ++i) {
yield args.map((e) => { return e[i]; }) as T;
}
}
如果我删除 as T
那么该函数可以工作,但我会丢失所有类型信息,如果没有,我会得到 A return 类型 [...T[]]
,但我想要 [...T]
.
例如zip([1, 2, 3], ['a', 'b', 'c'])
,应该有return类型的[number, string]
,但它有[number[], string[]]
您可能希望打字看起来像这样:
function* zip<T extends any[][]>(...args: T) {
for (let i = 0; i < Math.min(...args.map((e) => { return e.length })); ++i) {
yield args.map((e) => { return e[i]; }) as
{ [I in keyof T]: T[I] extends Array<infer E> ? E : never };
}
}
请注意,您希望 args
成为 个数组 的数组,对吗? args
的每个元素本身都应该有一个 length
属性 并且具有可通过数字索引访问的元素。所以 T extends any[][]
会为你做这件事(你甚至可能想放宽它以便它也接受 readonly
arrays,但我不会深入讨论。)
您不想 return T
生成器生成的每个元素,因为这是一个数组数组。如果你不注释 return 那么你会得到编译器认为 map()
产生的任何东西,这必然会比你正在寻找的更不精确(参见 T
.
相反,对于 T
的索引中的每个数字索引 I
,您希望在该索引处采用数组类型 T[I]
并且 return 该数组的元素类型。我们可以使用 mapped type to represent this transformation, because mapped types on tuples produce tuples。这就是 { [I in keyof T]: T[I] extends Array<infer E> ? E : never }
所做的。
好的,我们来测试一下:
for (const z of zip([1, 2, 3], ['a', 'b', 'c'])) {
console.log(z[0].toFixed(2) + ", " + z[1].toUpperCase())
// "1.00, A" "2.00, B" "3.00, C"
}
看起来不错;编译器认为 z
是 [number, string]
并让您相应地处理 z
的每个元素。