我在javascript中有一个函数,该函数采用0个或多个{string => async fn*(...) => ...}
类型的对象的数组(请注意*
-它们是异步生成器函数)。
该函数返回一个单个对象,该对象是每个输入对象中所有键的集合的并集,并且值是匹配的异步函数(不是生成器)每个输入对象的API-这里的要求是,如果两个输入对象共享一个键,则它们必须也完全共享功能原型。
例如:
function makeAggregateObject(objects) { ... }
const fish = {
swim: async function*(distance) { return distance > 10; }, // (number) => boolean
die: async function*() {} // () => void
};
const cat = {
die: async function*() { this.lives -= 1; } // () => void
};
const human = {
walk: async function*(steps, hop) { ... } // (number, boolean) => void
swim: async function*(distance) { return false; } // (number) => boolean
};
const aggregate = makeAggregateObject([
human, cat, fish
]);
console.log(aggregate);
/*
{
swim: async function(number) => boolean,
walk: async function(number, boolean) => void,
die: async function() => void
}
*/
正如我之前提到的,在这种情况下,我添加了从die(number) => boolean
到human
,这会被视为错误(当然,在Javascript中,没有实际执行此方法的方法,但是我想在Typescript中使用),因为原型(number) => boolean
确实与之前定义的() => void
原型不匹配。
在打字稿中甚至有可能吗?我将如何去做?
答案 0 :(得分:4)
您可以在makeAggregateObject
上使用通用类型来强制执行此操作。生成器的问题并不是真正的问题,它将强制所有通用属性具有兼容的类型。
我们将使用类型参数T
来在元组类型中捕获所有参数类型。然后,我们说参数必须为T
(这是将参数类型输入T
的方式),但也必须是一个类型,其中每个元素都是元组中所有类型的交集。基本上,如果打字稿从参数推断出T = [A, B]
,则该参数也必须是Array<Partial<A & B>>
。这将确保如果一个属性同时存在于A
和B
中,则它必须是两种属性类型的交集。因此,例如,如果die
在一个对象中是(n:number) => boolean
,而在另一个对象中是(s: string) => boolean
,则两个对象都将收到错误,指出该属性与((n:number) => boolean) & ((s: string) => boolean)
不兼容。
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
function makeAggregateObject<T extends [any] | any[]>(objects: T & Partial<UnionToIntersection<T[number]>>[]) :UnionToIntersection<T[number]> {
return null!
}
const fish = {
swim: async function*(distance: number) { return distance > 10; }, // (number) => boolean
die: async function*() { } // () => void
};
const cat = {
lives: 9,
die: async function*() { /* this.lives -= 1; */ } // () => void
};
const human = {
walk: async function*(steps: number, hop: boolean) { }, // (number, boolean) => void
swim: async function*(distance: number) { return false; }, // (number) => boolean
// die: (n : number) =>true // uncomment this and you get an error
};
const aggregate = makeAggregateObject([
human,
cat,
fish
]);
// const aggregate: {
// swim: (distance: number) => AsyncGenerator<never, boolean, unknown>;
// die: () => AsyncGenerator<never, void, unknown>;
// } & {
// lives: number;
// die: () => AsyncGenerator<never, void, unknown>;
// } & {
// walk: (steps: number, hop: boolean) => AsyncGenerator<never, void, unknown>;
// swim: (distance: number) => AsyncGenerator<never, boolean, unknown>;
// }
aggregate.swim(0) // ok
aggregate.die() // also ok
console.log(aggregate);
注意:您还可以将makeAggregateObject
的签名表示为:
function makeAggregateObject<T>(objects: T[] & Partial<UnionToIntersection<T>>[]) :UnionToIntersection<T> {
return null!
}
上面的签名在功能上在功能上等效,但是会产生措辞不同的错误消息。看看您认为哪一个更具可读性。都不是太好。我们可以想到一个更复杂的类型,该类型会在类型中引入其他信息,但会使类型复杂化,并且对类型系统有点滥用。如果您有兴趣,请告诉我,我可以提供这样的版本。
插件: UnionFromIntersection
摘自here,在这里您还可以阅读有关其工作原理的说明,以确保对出色的答案表示赞同。
修改
我们还可以从AsyncGenrator
返回类型中提取返回类型,并使用它来创建Promise
类型,作为问题大纲的新要求:
type MakeAggregateObject<T> = {
[P in keyof T]: T[P] extends (...p: infer P) => AsyncGenerator<never, infer R, any> ? (...p: P)=> Promise<R> : never
}
function makeAggregateObject<T extends [any] | any[]>(objects: T & Partial<UnionToIntersection<T[number]>>[]) :MakeAggregateObject<UnionToIntersection<T[number]>> {
return null!
}