เจเนอริก - ปัญหาในการขยายเมธอดที่มีการโอเวอร์โหลดใน 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 ที่ซับซ้อนขึ้นเล็กน้อย ผมรู้สึกว่านั่นก็โอเคแล้ว
