ฟังก์ชันห่อหุ้มคลาสในไทป์สคริปต์
นี่คือชั้นเรียนของฉัน:
class Foo {
static actions = ["func1", "func2"];
a: number;
func1(str: string) {
};
func2(n: number) {
};
func3() {}
}
เป้าหมายของฉันคือการทำให้การระบุประเภทใน TypeScript ของฟังก์ชันทั่วไป (ฟังก์ชัน wrapIt ด้านล่าง) ถูกต้อง ซึ่งฟังก์ชันนี้จะรับคลาสใดก็ได้ (เช่น Foo ที่มีชุดคุณสมบัติและฟังก์ชันแบบสุ่ม) และส่งคืนคลาสที่ทำตามเฉพาะการกระทำที่กำหนดจากฟิลด์คงที่ actions:
function wrapIt(MyClass) {
return class {
constructor() {
for (const actionName of MyClass.actions) {
const fn = MyClass.prototype[actionName];
this[actionName] = (...args: any[]) => {
console.log("do something here that just call the original function");
return fn.bind(this)(...args);
}
}
}
}
}
const NewClass = wrapIt(Foo)
const c = new NewClass();
c.func1("ok"); // typescript error: func1 is not known
func1 และ func2 ของ Result น่าจะเป็น func1 และ func2 ดั้งเดิม แต่ถูกห่อด้วยโค้ดบางส่วน
ฉันไม่มีปัญหาในการทำโค้ดแบบนั้นใน JavaScript ล้วนๆ แต่ใน Typescript มันเป็นนรกสำหรับฉัน :-|
สนามเด็กเล่น TS
คำตอบ #1
คุณจะต้องทำให้ actions เป็นค่าคงที่ในช่วงการคอมไพล์ใน Foo โดยการเติม as const:
class Foo {
// ...
static actions = ["func1", "func2"] as const;
// ... ^^^^^^^^^
}
จากนั้นเราสามารถทำได้โดยใช้พารามิเตอร์ประเภททั่วไปกับ wrapIt ก่อนอื่นมานิยามประเภทที่นำกลับมาใช้ใหม่ได้สำหรับฟังก์ชันตัวสร้าง:
type ConstructorFunction = new (...args: any[]) => any;
ตอนนี้เราจำเป็นต้องกำหนดประเภทของพารามิเตอร์ wrapIt ซึ่งต้องเป็น:
- ฟังก์ชันตัวสร้าง
- ด้วยคุณสมบัติ
actionsซึ่งเป็นอาเรย์ของชื่อเมธอดที่ถูกต้อง
เราจะต้องมีชื่อเมธอดเป็นพารามิเตอร์ชนิดของมันเอง ในการจำกัดชื่อเหล่านั้น เราจำเป็นต้องให้มันขยาย keyof ของชนิดของออบเจ็กต์ที่สร้างโดยคอนสตรัคเตอร์ ซึ่งเราสามารถได้มาผ่านชนิดยูทิลิตี้ InstanceType มันจะมีลักษณะดังนี้:
function wrapIt<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>>(
MyClass: Cls & { actions: readonly Method[] }
) {
// ...
}
(ฉันไม่ได้จำกัด actions เพียงแค่ชื่อ method ที่นั่น แค่ชื่อคุณสมบัติของอินสแตนซ์เท่านั้น คุณสามารถทำต่อไปได้หากไม่ต้องการให้ผู้ใช้ใส่ "a" ใน actions แต่ฉันไม่ได้สนใจเรื่องนั้นที่นี่ อย่างไรก็ตาม ฉันได้แนบหมายเหตุเกี่ยวกับวิธีทำด้านล่างแล้ว)
มาตอนนี้ถึงประเภทการคืนค่า ด้วยพารามิเตอร์ทั่วไปสองตัวและประเภทยูทิลิตี้ Pick ประเภทการคืนค่าค่อนข้างง่าย: Pick<InstanceType<Cls>, Method> มาลองกำหนดสิ่งนั้นเป็นบางสิ่งที่เราสามารถใช้ได้มากกว่าหนึ่งที่:
type WrapItReturn<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>> = new (
...args: any
) => Pick<InstanceType<Cls>, Method>;
จากนั้นเราจะใช้ WrapItReturn<Cls, method> เป็นประเภทการคืนค่า (เท่านั้น เราสามารถให้ TypeScript เดาเองได้ แต่ขอให้ชัดเจน) และใช้เป็นการยืนยันประเภทในสิ่งที่เรากำลังคืนค่า:
function wrapIt<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>>(
MyClass: Cls & { actions: readonly Method[] }
): WrapItReturn<Cls, Method> {
return class {
constructor() {
for (const actionName of MyClass.actions) {
const fn = MyClass.prototype[actionName];
(this as any)[actionName] = (...args: any[]) => {
console.log("do something here that just call the original function");
return fn.call(this, ...args); // Minor implementation tweak here
};
}
}
} as WrapItReturn<Cls, Method>;
}
นี่คือทั้งหมดรวมกัน:
type ConstructorFunction = new (...args: any[]) => any;
class Foo {
static actions = ["func1", "func2"] as const;
a!: number; // Added the ! just to avoid irrelevant initialization error, not part of solution
func1(str: string) {
console.log(str);
}
func2(n: number) {
console.log(n);
}
func3() {}
}
type WrapItReturn<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>> = new (
...args: any
) => Pick<InstanceType<Cls>, Method>;
function wrapIt<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>>(
MyClass: Cls & { actions: readonly Method[] }
): WrapItReturn<Cls, Method> {
return class {
constructor() {
for (const actionName of MyClass.actions) {
const fn = MyClass.prototype[actionName];
(this as any)[actionName] = (...args: any[]) => {
console.log("do something here that just call the original function");
return fn.call(this, ...args); // Minor implementation tweak here
};
}
}
} as WrapItReturn<Cls, Method>;
}
const NewClass = wrapIt(Foo);
const c = new NewClass();
c.func1("ok"); // typescript error: func1 is not known
ลิงก์สนามเด็กเล่น
หากคุณต้องการจำกัดเนื้อหาของ actions ให้เป็นเฉพาะคีย์ของ methods เท่านั้น คุณสามารถใช้ประเภทยูทิลิตี้นี้ได้:
type FunctionKeys<ObjType> = keyof {
[Key in keyof ObjType as ObjType[Key] extends (...args: any[]) => any ? Key : never]: Key;
};
จากนั้นใช้ FunctionKeys<InstanceType<Cls>> แทน keyof InstanceType<Cls>:
type WrapItReturn<Cls extends ConstructorFunction, Method extends FunctionKeys<InstanceType<Cls>>> = new (
...args: any
) => Pick<InstanceType<Cls>, Method>;
function wrapIt<Cls extends ConstructorFunction, Method extends FunctionKeys<InstanceType<Cls>>>(
MyClass: Cls & { actions: readonly Method[] }
): WrapItReturn<Cls, Method> {
// ...
}
ลิงก์สนามเด็กเล่น
ในกรณีนั้น หากคุณเพิ่ม "a" ลงใน actions คุณจะเห็นว่า Foo ไม่ใช่อาร์กิวเมนต์ที่ถูกต้องสำหรับ wrapIt อีกต่อไป เนื่องจากคุณสมบัติที่ไม่ใช่ฟังก์ชันถูกระบุใน actions
