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