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

typescript - TS2322: 'T' อาจถูกกำหนดให้เป็นชนิดใดก็ได้ซึ่งอาจไม่เกี่ยวข้องกับ 'SuperClass'


ฉันมีคลาสหลักซึ่งมีซับคลาสจำนวนมาก

class SuperClass{
    constructor(){}
}
class SubClass1 extends SuperClass{
    constructor(){super()}
    SubClass1Prop(){}
}
class SubClass2 extends SuperClass{
    constructor(){super()}
    SubClass2Prop(){}
}

ฉันมีรายการที่ประกอบด้วยหลายซับคลาสต่าง ๆ และฉันได้ประกาศประเภทของมันเป็น SuperClass[] เพราะพวกมันมีบรรพบุรุษร่วมกัน

let list:SuperClass[] = [];
list.push(new SubClass1());
list.push(new SubClass2());

ฉันมีฟังก์ชันที่รับ SubClass เป็นพารามิเตอร์ จากนั้นกรองรายการเพื่อคืนค่าอินสแตนซ์ของ SubClass นั้น

การประกาศชนิดจากฟังก์ชันนี้ถูกนำมาจากคำตอบใน SO นี้ (ชนิดของฟังก์ชันที่รับคลาสเป็นอาร์กิวเมนต์ และคืนค่าวัตถุที่เป็นอินสแตนซ์ของคลาสนั้น) โดยมีความแตกต่างเล็กน้อยตรงที่ฉันกำลังคืนค่าเป็นรายการแทนที่จะเป็นวัตถุเพียงตัวเดียว

ข้อผิดพลาดอยู่ตรงนี้ ฉันควบคุมประเภทที่สามารถส่งเข้าไปในฟังก์ชัน filterList ได้ และสามารถรับประกันได้ว่ามันจะเป็นลูกของ SuperClass เสมอ อย่างไรก็ตาม TypeScript ไม่มีวิธีใดที่จะรู้เรื่องนี้ ฉันจะประกาศอย่างไรว่า className ใดๆ ที่ฉันส่งไปยังฟังก์ชันนี้ต้องเป็นลูกของ SuperClass เสมอ และไม่สามารถเป็น "ประเภทใดก็ได้"?

function filterList<T>(className: { new():T }):T[] {
    //TS2322: Type 'SuperClass[]' is not assignable to type 'T[]'.   
    // Type 'SuperClass' is not assignable to type 'T'.     
    // 'T' could be instantiated with an arbitrary type which could be unrelated to 'SuperClass'
    return list.filter(obj => obj instanceof className);
}

ตัวอย่างสถานการณ์การใช้งานสำหรับฟังก์ชันข้างต้น (ลบการตรวจสอบความปลอดภัยจากค่า null ออกเพื่อให้เรียบง่ายสำหรับการสาธิต)

ฉันต้องการให้ filteredList ถูกระบุอย่างถูกต้องว่าเป็นชนิด SubClass1[] และไม่ใช่ชนิด SuperClass[] หรือ any[] โดยไม่ต้องทำการแปลงชนิดอย่างชัดเจนโดยใช้คีย์เวิร์ด "as" ปัจจุบันความต้องการทั้งหมดนี้ถูกตอบสนองแล้ว แต่เกิดข้อผิดพลาด TS ในฟังก์ชันด้านบน วิธีแก้ไขใด ๆ ต้องไม่ทำให้การตรวจสอบชนิดที่นี่ผิดพลาด

let filteredList = filterList(SubClass1);
filteredList[0].SubClass1Prop();

คำตอบ #1

แม้ว่ามันจะง่ายกว่ามาก คุณสามารถใช้ InstanceType ร่วมกับพารามิเตอร์เจเนอริกและตัวตรวจสอบชนิดเพื่อให้ได้ผลลัพธ์แบบเดียวกัน:

function filterList<T extends { new (...args: any): any }>(className: T): InstanceType<T>[] {
    const isT = (potentialT: any): potentialT is InstanceType<T> => {
        return potentialT instanceof className;
    }
    return list.filter<InstanceType<T>>(isT);
}

สนามเด็กเล่น

คำตอบ #2

list เป็นชนิด SuperClass[] ดังนั้นการกรองมันจะได้กลับมาเป็น SuperClass[] อีกชุดหนึ่ง ดังนั้นฟังก์ชันจึงคืนค่าชนิด SuperClass[] อย่างไรก็ตาม ตามชนิดของฟังก์ชัน มันจริง ๆ คืนค่าชนิด T[] ถ้า T เป็นไปตาม SuperClass เช่นในตัวอย่างที่คุณเรียกใช้งาน ก็ถือว่าดี แต่ถ้า T ไม่เป็นไปตาม SuperClass ลิสต์ที่ถูกกรองซึ่งคุณคืนค่าจะไม่สามารถกำหนดค่าให้กับชนิดค่าที่แท้จริงของฟังก์ชันได้ เพื่อแก้ไขสิ่งนี้ คุณเพียงแค่ต้องมั่นใจว่า T เป็นไปตาม SuperClass ซึ่งสามารถทำได้ง่ายด้วยพารามิเตอร์ชนิด T extends SuperClass

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

คุณจะสังเกตได้ว่าการทำการเปลี่ยนแปลงนั้น อย่างไรก็ตาม จะทำให้คุณพบข้อผิดพลาดอีกข้อหนึ่ง:

'SuperClass' สามารถกำหนดให้กับข้อจำกัดของชนิด 'T' ได้ แต่ 'T' อาจถูกสร้างขึ้นด้วยชนิดย่อยอื่นของข้อจำกัด 'SuperClass'

ปัญหาที่นี่เริ่มต้นเหมือนเดิมก่อนหน้านี้: ฟังก์ชันส่งค่ากลับชนิด SuperClass[] แต่ถูกระบุประเภทว่าส่งค่ากลับเป็น T[] ถ้า T คือ SuperClass ก็จะไม่มีปัญหา แต่ถ้าไม่ใช่ และ T เป็นซับคลาสของ SuperClass ที่มีสมาชิกเพิ่มเติม SuperClass[] จะไม่สามารถกำหนดค่าให้ T[] ได้

มันอาจดูขัดแย้งกับสัญชาตญาณที่รายชื่อที่กรองออกมาจริงๆ มีประเภทเป็น SuperClass[] เนื่องจากคุณกำลังกรองทุกอย่างออกจากอาร์เรย์ที่ไม่ใช่ประเภท T อย่างชัดเจน ซึ่งบ่งบอกว่าอาร์เรย์ที่กรองออกมาควรมีประเภทเป็น T[] แต่จริงๆ แล้วมันไม่ได้ทำงานแบบนั้น ตัวตรวจสอบประเภท obj instanceof className สร้างความหมายเชิงสัญลักษณ์ว่า obj เป็นประเภท T และตั้งแต่จุดนั้นเป็นต้นมาใน callback ตัวคอมไพเลอร์ก็จะรู้เรื่องนั้น อย่างไรก็ตาม ความหมายนั้นไม่ได้ถูกส่งให้กับผู้ที่เรียกใช้ callback นั้น การเรียก Array.prototype.filter() ของคุณไม่รู้ว่าประเภทใดที่ตอบสนองต่อ callback ของมัน เพราะ callback ไม่ได้บอกมัน สิ่งที่มันรู้คืออาร์เรย์ที่มันกำลังกรองมีประเภทเป็น SuperClass[] ดังนั้นสิ่งที่ดีที่สุดที่มันสามารถบอกได้คืออาร์เรย์ที่ได้จากการกรองก็มีประเภทเป็น SuperClass[] เช่นกัน

วิธีแก้ปัญหานี้ง่ายมาก: คุณเพียงแค่ต้องระบุ type predicate (ดูเอกสาร) บน callback ของคุณ จากสิ่งนี้ filter() จะทราบว่าประเภทใดที่ตอบสนอง callback ของมัน ซึ่งช่วยให้สามารถบอกได้อย่างถูกต้องว่าประเภทของอาร์เรย์ที่ได้คืออะไร

const foo: SuperClass[] = list.filter(obj => obj instanceof className)
const bar: T[] = list.filter((obj): obj is T => obj instanceof className)

ดังนั้น เมื่อใช้การแก้ไขทั้งสองนี้ ฟังก์ชัน filterList() ของคุณตอนนี้จะดูเหมือนดังนี้:

function filterList<T extends SuperClass>(className: { new():T }):T[] {
    return list.filter((obj): obj is T => obj instanceof className);
}

ข้างๆ

ฉันรู้สึกสงสัยเกี่ยวกับสิ่งที่คุณพยายามทำที่นี่ โดยในเชิงนามธรรม ฉันไม่แน่ใจว่านี่เป็นความคิดที่ดี คุณควรรู้ว่าการสืบทอดคลาสเองไม่ได้มีความหมายเชิงนิพจน์ในแง่ของประเภท หากกำหนด class A {} แล้ว new A จะสามารถกำหนดค่าให้กับประเภท SuperClass ได้ มันไม่ใช่เรื่องของ A ที่เกี่ยวข้องกับ SuperClass จริง ๆ แต่เป็นเรื่องของ A ที่ตอบสนองต่อ SuperClass ฉันไม่คิดว่ามีวิธีใดในฟังก์ชัน filterList ของคุณที่จะบังคับให้ T เป็นซับคลาสของ SuperClass ได้จริง