如何声明可变数量的参数类型的交集类型
How to declare the intersection type of variable amount of argument types
我有一个函数 merge
,它接受可变数量的对象作为参数,并创建一个新对象,其类型是参数类型的交集,有点像 Object.assign()
,所以 merge({ a: 1 }, { b: 2 })
将 return 类型为 { a: number, b: number}
的对象。
想象一下这样的实现:
function merge(...objects) {
// Something like
return Object.assign({}, ...objects)
}
我正在寻找一种声明其类型的方法,该方法允许我将类型信息保存在 returned 对象中。
我只能想象像这样重载定义:
function merge<A>(a: A): A;
function merge<A, B>(a: A, b: B): A & B;
function merge<A, B, C>(a: A, b: B, c: C): A & B & C;
// etc.
这不太方便,因为预计将使用超过五个参数调用此函数。
谢谢!
您可以使用单个类型参数 T
对应于其所有元素类型的 tuple type of the rest parameter to merge()
. Then you need to come up with a way to take a tuple type and calculate the intersection。我们称该类型函数为 IntersectArrayElements<T>
。所以 merge()
看起来像:
function merge<T extends any[]>(
...objects: T
): IntersectArrayElements<T> {
return Object.assign({}, ...objects)
}
现在我们只需实施 IntesectArrayElements<T>
。有几种方法可以做到这一点:
一个相对简单的实现是写一个 recursive conditional type that uses variadic tuple types:
type IntersectArrayElements<T extends any[]> =
T extends [infer F, ...infer R] ? F & IntersectArrayElements<R> : unknown;
它将元组T
拆分为它的第一个元素F
和元组的其余部分R
,然后结果F
与[=25相交=],如果 T
已经是空的(或者是数组而不是元组),那么它会生成 the unknown
type,它会被吸收到所有交叉点中。
如果您有很长的元组,上述实现可能会达到递归限制(大约 25、50 或 100 个元素)。对 TypeScript 4.5+ 的上述改进是将其重写为 tail-recursive type function,TypeScript 可以更有效地处理最多 1000 个元素的元组:
type IntersectArrayElements<T extends any[], A = unknown> =
T extends [infer F, ...infer R] ? IntersectArrayElements<R, A & F> : A;
这基本上是相同的算法,除了它在累加器类型参数中收集结果 A
。
递归实现可能是最容易理解的,但递归可能有点昂贵,尤其是与其他本身可能是递归的类型函数一起使用时。如果你可以在没有递归的情况下实现某些东西,那么最好这样做。并且有一种不用递归来写 IntersectArrayElements<T>
的方法,尽管是以一种不太明显的方式:
type IntersectArrayElements<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void
}[number] extends (x: infer I) => void ? I : never;
这首先在它们的参数类型中 mapping the tuple into a new form where each element becomes the parameter type to a function. So [{a: string}, {b: number}]
becomes [(x: {a: string})=>void, (x: {b: number})=>void]
. Then we index into it with number
to get a union of these functions... so now we'd have ((x: {a: string})=>void)|((x: {b: number})=>void)
. And from this we use conditional type inference to infer a single parameter type for that union of functions. Because function types are contravariant 起作用,编译器推断出 交集 而不是并集...所以现在我们有 {a: string} & {b: number}
.简单吧?
对于这三种实现中的任何一种,merge()
都会出现以下行为:
const a = { a: "hello" };
const b = { b: 123 };
const c = { c: true };
const mA = merge(a); // {a: string}
const mAB = merge(a, b); // {a: string} & {b: number}
const mABC = merge(a, b, c); // {a: string} & {b: number} & {c: boolean}
const mAandBorC =
merge(a, Math.random() < 0.5 ? b : c) // {a: string} & ({b: number} | {c: boolean})
这看起来就是你想要的,万岁!
一个小警告:当您使用一组编译器已知其顺序和类型的参数调用 merge()
时,这会很好地工作。如果编译器无法计算出参数的确切数量和顺序,那么您可能会得到不太理想的输出类型:
const forgotOrder = [a, b];
const mOops = merge(...forgotOrder);
这里,forgotOrder
推断出的类型是无序数组类型Array<{a: string} | {b: number}>
。根据我们称为 merge()
的类型,编译器不知道 forgotOrder
中有多少元素,也不知道 {a: string}
和 {b: number}
元素是否都存在。因此 mOops
的类型是 IntersectArrayElements<Array<{a: string} | {b: number}>>
,它要么是 {a: string} | {b: number}
本身,要么可能是 unknown
,具体取决于您在上面使用的实现方式。不清楚这种奇怪的调用应该 是什么类型,所以这就是为什么我说这是次要的。可以尝试加强 merge()
以应对这种情况,但这可能超出了问题的范围。
我有一个函数 merge
,它接受可变数量的对象作为参数,并创建一个新对象,其类型是参数类型的交集,有点像 Object.assign()
,所以 merge({ a: 1 }, { b: 2 })
将 return 类型为 { a: number, b: number}
的对象。
想象一下这样的实现:
function merge(...objects) {
// Something like
return Object.assign({}, ...objects)
}
我正在寻找一种声明其类型的方法,该方法允许我将类型信息保存在 returned 对象中。
我只能想象像这样重载定义:
function merge<A>(a: A): A;
function merge<A, B>(a: A, b: B): A & B;
function merge<A, B, C>(a: A, b: B, c: C): A & B & C;
// etc.
这不太方便,因为预计将使用超过五个参数调用此函数。
谢谢!
您可以使用单个类型参数 T
对应于其所有元素类型的 tuple type of the rest parameter to merge()
. Then you need to come up with a way to take a tuple type and calculate the intersection。我们称该类型函数为 IntersectArrayElements<T>
。所以 merge()
看起来像:
function merge<T extends any[]>(
...objects: T
): IntersectArrayElements<T> {
return Object.assign({}, ...objects)
}
现在我们只需实施 IntesectArrayElements<T>
。有几种方法可以做到这一点:
一个相对简单的实现是写一个 recursive conditional type that uses variadic tuple types:
type IntersectArrayElements<T extends any[]> =
T extends [infer F, ...infer R] ? F & IntersectArrayElements<R> : unknown;
它将元组T
拆分为它的第一个元素F
和元组的其余部分R
,然后结果F
与[=25相交=],如果 T
已经是空的(或者是数组而不是元组),那么它会生成 the unknown
type,它会被吸收到所有交叉点中。
如果您有很长的元组,上述实现可能会达到递归限制(大约 25、50 或 100 个元素)。对 TypeScript 4.5+ 的上述改进是将其重写为 tail-recursive type function,TypeScript 可以更有效地处理最多 1000 个元素的元组:
type IntersectArrayElements<T extends any[], A = unknown> =
T extends [infer F, ...infer R] ? IntersectArrayElements<R, A & F> : A;
这基本上是相同的算法,除了它在累加器类型参数中收集结果 A
。
递归实现可能是最容易理解的,但递归可能有点昂贵,尤其是与其他本身可能是递归的类型函数一起使用时。如果你可以在没有递归的情况下实现某些东西,那么最好这样做。并且有一种不用递归来写 IntersectArrayElements<T>
的方法,尽管是以一种不太明显的方式:
type IntersectArrayElements<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void
}[number] extends (x: infer I) => void ? I : never;
这首先在它们的参数类型中 mapping the tuple into a new form where each element becomes the parameter type to a function. So [{a: string}, {b: number}]
becomes [(x: {a: string})=>void, (x: {b: number})=>void]
. Then we index into it with number
to get a union of these functions... so now we'd have ((x: {a: string})=>void)|((x: {b: number})=>void)
. And from this we use conditional type inference to infer a single parameter type for that union of functions. Because function types are contravariant 起作用,编译器推断出 交集 而不是并集...所以现在我们有 {a: string} & {b: number}
.简单吧?
对于这三种实现中的任何一种,merge()
都会出现以下行为:
const a = { a: "hello" };
const b = { b: 123 };
const c = { c: true };
const mA = merge(a); // {a: string}
const mAB = merge(a, b); // {a: string} & {b: number}
const mABC = merge(a, b, c); // {a: string} & {b: number} & {c: boolean}
const mAandBorC =
merge(a, Math.random() < 0.5 ? b : c) // {a: string} & ({b: number} | {c: boolean})
这看起来就是你想要的,万岁!
一个小警告:当您使用一组编译器已知其顺序和类型的参数调用 merge()
时,这会很好地工作。如果编译器无法计算出参数的确切数量和顺序,那么您可能会得到不太理想的输出类型:
const forgotOrder = [a, b];
const mOops = merge(...forgotOrder);
这里,forgotOrder
推断出的类型是无序数组类型Array<{a: string} | {b: number}>
。根据我们称为 merge()
的类型,编译器不知道 forgotOrder
中有多少元素,也不知道 {a: string}
和 {b: number}
元素是否都存在。因此 mOops
的类型是 IntersectArrayElements<Array<{a: string} | {b: number}>>
,它要么是 {a: string} | {b: number}
本身,要么可能是 unknown
,具体取决于您在上面使用的实现方式。不清楚这种奇怪的调用应该 是什么类型,所以这就是为什么我说这是次要的。可以尝试加强 merge()
以应对这种情况,但这可能超出了问题的范围。