表示函数,该函数采用对象数组并返回具有所有键的并集的单个对象

时间:2020-08-31 23:27:35

标签: typescript types

我在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) => booleanhuman,这会被视为错误(当然,在Javascript中,没有实际执行此方法的方法,但是我想在Typescript中使用),因为原型(number) => boolean确实与之前定义的() => void原型不匹配。

在打字稿中甚至有可能吗?我将如何去做?

1 个答案:

答案 0 :(得分:4)

您可以在makeAggregateObject上使用通用类型来强制执行此操作。生成器的问题并不是真正的问题,它将强制所有通用属性具有兼容的类型。

我们将使用类型参数T来在元组类型中捕获所有参数类型。然后,我们说参数必须为T(这是将参数类型输入T的方式),但也必须是一个类型,其中每个元素都是元组中所有类型的交集。基本上,如果打字稿从参数推断出T = [A, B],则该参数也必须是Array<Partial<A & B>>。这将确保如果一个属性同时存在于AB中,则它必须是两种属性类型的交集。因此,例如,如果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);

Playground Link

注意:您还可以将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!
}

Playground Link