ค้นหาเว็บไซต์

ฟังก์ชันห่อหุ้มคลาสในไทป์สคริปต์


นี่คือชั้นเรียนของฉัน:

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