網站搜尋

我如何讓一個合併類函數在 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 函數

此外,我冒著這個問題會被點踩的風險,如果那是你,也沒關係,你完全可以點踩我,但至少讓我知道這是否真的有可能做到,這樣我至少可以帶著對成功的現實預期繼續努力研究這個問題。

答案#1

你的 mergeClasses 有限制,因為你使用了泛型來描述每個單獨的類。我們可以通過使用單個泛型 C 來描述構造函數的元組類型,使它接受任意數量的類。

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

為了獲取 args 陣列的類型,我們需要將映射類型應用到元組上。

為了獲取組合實例的類型,我們使用另一個映射類型來獲取所有實例的元組。對其使用 [number] 索引會給我們一個所有實例類型的聯合。我們可以應用 @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

Typescript 遊樂場連結


這是以你之前使用的相同格式接受參數:([], [], [])。我並不喜歡這種格式,因為打出一堆空陣列感覺很雜亂。如果每個建構函數都可以接受不同數量的參數,要將這些參數展平成單一的 ...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

Typescript 遊樂場連結


你也可以要求它們都有一個單一的參數,即一個 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);

Typescript 遊樂場連結