首页 > 解决方案 > 如何使合并类函数能够在 Typescript 中接受 n 个类参数?

问题描述

所以我有一个 mergeClass 函数,它接受各种类并将它们合并,这样一个变量将具有所有这些类成员的所有属性,并且它都是实例化的。我目前这样做的方式是我必须合并三个类,但假设我只需要 2、5 或 n 个可以合并的类?我将如何做到这一点?基本上我希望能够通过任意数量的课程。我开始认为这是不可能的,如果是这样,请告诉我。谢谢。

type Constructable = new (...args: any[]) => any;

const mergeClasses = <S extends Constructable, T extends Constructable, P extends Constructable>(class1: S, class2: T, class3: P) =>
    <Si extends InstanceType<S> = InstanceType<S>, 
    Ti extends InstanceType<T> = InstanceType<T>,
    Pi extends InstanceType<P> = InstanceType<P>>
    (args1: ConstructorParameters<S>, args2: ConstructorParameters<T>, args3: ConstructorParameters<P>): Si & Ti & Pi => {
        const obj1 = new class1(...args1);
        const obj2 = new class2(...args2);
        const obj3 = new class3(...args3);
        for (const p in obj2) {
            obj1[p] = obj2[p];
        }
        for(const p in obj3){
            obj1[p] = obj3[p];
        }
        return obj1 as Si & Ti & Pi;
};

//I need something like this maybe in my mergeClasses function?
for(let i = 0; i<classes.length; i++){
   let obj = classes[i]
   const obj1 = new obj();
   for (const p in obj) {
     obj1[p] = obj[p];
   } 
}

const mergeE = mergeClasses(B, C, D);
const e = mergeE<B, C, D>([], [], []);

这是一个 Typescript Playground MergeClasses 函数

另外,我冒着对这个问题表示反对的风险,如果是你,那很好,非常欢迎你对我表示反对,但至少让我知道这是否可能做到,这样我就可以至少继续以对成功的现实期望来解决这个问题。

标签: typescript

解决方案


mergeClasses是有限的,因为你已经使用泛型来描述每个单独的类。C我们可以通过使用单个泛型来描述元组类型的构造函数,使其接受任意数量的类。

const mergeClasses = <C extends Constructable[]>(...classes: C) =>

为了获取 args 数组的类型,我们需要将映射类型应用于元组。

为了获取组合实例的类型,我们使用另一个映射类型来获取所有实例的元组。索引[number]on 为我们提供了所有实例类型的联合。我们可以应用@jcalz 著名的UnionToIntersection实用程序类型来获取实例类型的交集。

type Constructable = new (...args: any[]) => any;

// mapped type to apply ConstructorParameters to a tuple
type ToArgs<T> = {
    [K in keyof T]: T[K] extends new (...args: infer A) => any ? A : never;
}

// mapped type to apply InstanceType to a tuple
type ToInstance<T> = {
    [K in keyof T]: T[K] extends new (...args: any[]) => infer I ? I : never;
}

// utility type to convert a union to an instersection
type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

// the type of the instance that the merged class creates
type CombinedInstance<C extends Constructable[]> = UnionToIntersection<ToInstance<C>[number]>

const mergeClasses = <C extends Constructable[]>(...classes: C) =>
    (...argsArrays: ToArgs<C>): CombinedInstance<C> => {
        const obj = {} as CombinedInstance<C>
        classes.forEach((someClass, i) => {
            const newObj = new someClass(argsArrays[i]);
            for (const p in newObj) {
                // p has type `string` so we need to assert
                obj[p as keyof CombinedInstance<C>] = newObj[p];
            }
        });
        return obj;
    };

const mergeE = mergeClasses(B, C, D);  // type: (argsArrays_0: [], argsArrays_1: [], argsArrays_2: []) => B & C & D
const e = mergeE([], [], []); // type: B & C & D
console.log(e); // logs properties of B, C, and D

打字稿游乐场链接


这是接受与您之前的格式相同的 args ([], [], [])。我不喜欢这种格式,因为输入一堆空数组感觉很草率。...args如果每个构造函数都可以接受不同数量的参数,那么在确保将正确的参数传递给正确的构造函数的同时,很难将其扁平化。

如果你所有的类都没有参数,你绝对可以让这个更干净。

const mergeClasses = <C extends (new () => any)[]>(...classes: C) =>
    (): CombinedInstance<C> => {
        const obj = {} as CombinedInstance<C>
        classes.forEach(someClass => {
            const newObj = new someClass();
            for (const p in newObj) {
                // p has type `string` so we need to assert
                obj[p as keyof CombinedInstance<C>] = newObj[p];
            }
        });
        return obj;
    };

const mergeE = mergeClasses(B, C, D);  // type: () => B & C & D
const e = mergeE(); // type: B & C & D

打字稿游乐场链接


您还可以要求它们都具有一个作为 props 对象的参数,并让您的合并类接受具有所有属性的对象(假设没有一个具有冲突的属性类型)。

// now takes only 1 argument
type Constructable = new (props: any) => any;

// source: https://stackoverflow.com/a/50375286/10431574
type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

// mapped type to apply InstanceType to a tuple
type ToInstance<T> = {
    [K in keyof T]: T[K] extends new (...args: any[]) => infer I ? I : never;
}

// now extracts only 1 argument
type ToArgs<T> = {
    [K in keyof T]: T[K] extends new (props: infer A) => any ? A : never;
}

// the type of the instance that the merged class creates
type CombinedInstance<C extends Constructable[]> = UnionToIntersection<ToInstance<C>[number]>

// need to combine all args to one like with the instances
type CombinedArgs<C extends Constructable[]> = UnionToIntersection<ToArgs<C>[number]>

const mergeClasses = <C extends Constructable[]>(...classes: C) =>
    (props: CombinedArgs<C>): CombinedInstance<C> => {
        const obj = {} as CombinedInstance<C>
        classes.forEach((someClass, i) => {
            // pass the whole props object to each constructor
            const newObj = new someClass(props);
            for (const p in newObj) {
                // p has type `string` so we need to assert
                obj[p as keyof CombinedInstance<C>] = newObj[p];
            }
        });
        return obj;
    };

class B {
    b: string;

    constructor({ b }: { b: string }) {
        this.b = b;
    }
}

class C {
    c: string;

    constructor({ c }: { c: string }) {
        this.c = c;
    }
}

class D {
    d: string;

    constructor({ d }: { d: string }) {
        this.d = d;
    }
}

const mergeE = mergeClasses(B, C, D);  // type: (props: { b: string; } & { c: string; } & { d: string; }) => B & C & D
const e = mergeE({ b: "b prop", c: "c prop", d: "d prop" }); // type: B & C & D
console.log(e);

打字稿游乐场链接


推荐阅读