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

เจเนอริก - ปัญหาในการขยายเมธอดที่มีการโอเวอร์โหลดใน TypeScript


ในคลาสแม่ ผมกำหนดเมธอดโดยมีพารามิเตอร์ต่างกัน: ดัชนีในสตอร์หรือองค์ประกอบ และขึ้นอยู่กับพารามิเตอร์ ผมจะเอาองค์ประกอบจากสตอร์ตามดัชนีหรือตรง ๆ ใช้องค์ประกอบที่ให้มา
ดังนั้น ผมจึงใช้การโอเวอร์โหลดเพื่อกำหนดเมธอดเหล่านี้:

class Parent<T> {
  items: T[] = [];
  item: T = {} as T;

  applyMethod(index: number): void;
  applyMethod(item: T): void;
  applyMethod(param: number | T): void {
    if (typeof param === 'number') {
      param = this.items[param];
    }
    this.item = param;
  }
}

class Child1<T> extends Parent<T> {
  // In my application, I actually add an id: string overload.
  override applyMethod(param: number | T): void {
    // Do something more before calling:
    super.applyMethod(param);
  }
}

class Child2<T> extends Parent<T> {
  override applyMethod(param: number | T): void {
    if (typeof param === 'number') {
      super.applyMethod(param);
    } else {
      super.applyMethod(param);
    }
  }
}

ดูมันใน TS Playground ด้วยเช่นกัน

ใน Child1 ฉันมีข้อผิดพลาด:

No overload matches this call.\
  Overload 1 of 2, '(index: number): void', gave the following error.\
    Argument of type 'number | T' is not assignable to parameter of type 'number'.\
      Type 'T' is not assignable to type 'number'.
  Overload 2 of 2, '(item: T): void', gave the following error.\
    Argument of type 'number | T' is not assignable to parameter of type 'T'.\
      'T' could be instantiated with an arbitrary type which could be unrelated to 'number | T'.(2769

ฉันสามารถแก้ปัญหาชั่วคราวได้ตามที่แสดงใน Child2 แต่ก็แปลก ๆ/โง่เล็กน้อย

อีกวิธีแก้ปัญหาหนึ่งคือการกำหนด applyMethod(param: any): void {} แทน number | T มันเป็นวิธีแบบคลาสสิกและนั่นคือสิ่งที่ฉันทำตอนแรก แต่ฉันพบว่า TS เห็นเพียงการกำหนดพารามิเตอร์ param: any ดังนั้นฉันจึงสูญเสียการพิมพ์ข้อมูล

ฉันเลือกวิธีอื่น เรียก super.applyMethod(param as any); อย่างน้อย อินพุตก็ยังคงมีชนิดข้อมูลของพวกมัน

มีวิธีอื่นที่ดูงดงามกว่านี้ไหม? ฉันพลาดอะไรไปหรือเปล่า?

คำตอบ #1

การโอเวอร์โหลดใน TypeScript มีข้อจำกัดหลายประการที่ทำให้การใช้งานยาก ข้อจำกัดที่คุณกำลังเผชิญอยู่คือปัจจุบันตัวตรวจสอบประเภทจะตรวจสอบการเรียกใช้ไปยังลายเซ็นการเรียกเพียง หนึ่ง แบบเท่านั้น มันจะไม่พยายาม รวม ลายเซ็นการเรียก

ถ้าคุณมีลายเซ็นการเรียกสองแบบเช่น (x: string) => string และ (y: number) => number คุณสามารถเรียก f("") หรือ f(0) ได้ แต่ไม่สามารถเรียก f(Math.random()<0.5?"":0) ได้ เพราะในกรณีหลังจะต้องให้ตัวคอมไพเลอร์รวมลายเซ็นการเรียกทั้งสองแบบเข้าเป็นบางอย่างเช่น (xOrY: string | number) => string | number กล่าวคือ จะต้องสร้างลายเซ็นการเรียกที่สอดคล้องกับสหภาพของพารามิเตอร์ของแต่ละลายเซ็นการเรียก

มีคำขอคุณสมบัติที่เปิดอยู่ที่ microsoft/TypeScript#14107 ซึ่งขอการสนับสนุนดังกล่าว แต่ตอนนี้ยังไม่เป็นส่วนหนึ่งของภาษา


จนกว่าจะมีการนำสิ่งนั้นไปใช้งาน คุณจะต้องหาทางหลีกเลี่ยงมัน หากคุณสามารถหลีกเลี่ยงการโอเวอร์โหลดได้ทั้งหมด นั่นมักจะเป็นวิธีที่ดีที่สุด เนื่องจากมีหลายวิธีที่การโอเวอร์โหลดไม่ได้รับการสนับสนุนอย่างดีโดยระบบประเภท (Typescript: ReturnType ของฟังก์ชันที่มีการโอเวอร์โหลดและตัวอย่างอื่น ๆ อีกมากมายสามารถพบได้)

หากคุณต้องการเก็บการโอเวอร์โหลดไว้ หนึ่งในวิธีแก้ปัญหา (ตามที่กล่าวในปัญหา GitHub) คือการเพิ่มโอเวอร์โหลดด้วยตนเอง (ที่ด้านล่าง) ที่รวมยูเนียนที่เกี่ยวข้อง:

class Parent<T> {
  items: T[] = [];
  item: T = {} as T;

  applyMethod(index: number): void;
  applyMethod(item: T): void;
  applyMethod(param: number | T): void; // <-- add catch-all overload
  applyMethod(param: number | T): void {
    if (typeof param === 'number') {
      param = this.items[param];
    }
    this.item = param;
  }
}

ตอนนี้การเรียกใช้ภายในคลาสลูกทำงานตามที่เขียน เพราะมันสามารถระบุไปยังลายเซ็นการเรียกครั้งที่สามจากคลาสแม่ได้:

class Child1<T> extends Parent<T> {
  override applyMethod(param: number | T): void {
    super.applyMethod(param); // okay
  }
}

ลิงก์ Playground ไปยังโค้ด

คำตอบ #2

ตามที่ jcalz แนะนำ การใช้ overload อาจมีปัญหา
ข้อดีอย่างหนึ่งของมันคือสามารถปรับแต่งความคิดเห็น JSDoc ให้ขึ้นอยู่กับ overload ได้:

/**
 * Set the active item to the item at the specified index .
 *
 * @param index - The index of the item to be set as active.
 */
setActiveItem(index: number): void;
/**
 * Set the active item to the specified item.
 *
 * @param item - The item to be set as active.
 */
setActiveItem(item: I): void;
setActiveItem(param: number | I): void {
}

ข้อดีอีกอย่างคือพวกมันสามารถอนุญาตให้มีประเภทการคืนค่าที่แตกต่างกันขึ้นอยู่กับอินพุต แต่ถ้าไม่มีโค้ดเก่าอยู่ ผมคิดว่ามันดีกว่าที่จะมีชื่อเมธอดที่แตกต่างกันในกรณีนี้

เมื่อฉันลบการโอเวอร์โหลดออกแล้ว ฉันต้องสร้างเอกสารที่ทั่วไปมากขึ้น:

/**
 * Set the active item to the given item or the one at the specified index.
 *
 * @param param - The item itself or the index of the item to be set as active.
 */
setActiveItem(param: number | I): void {
}

มันช่วยให้โค้ดง่ายขึ้น อย่างน้อยก็ช่วยให้โค้ดอ่านง่ายขึ้นในท้ายที่สุด แต่ต้องแลกกับ JSDoc ที่ซับซ้อนขึ้นเล็กน้อย ผมรู้สึกว่านั่นก็โอเคแล้ว

บทความที่เกี่ยวข้อง