มีวิธีใดที่จะสื่อสาร TypeScript type guard นี้ ซึ่งเกี่ยวข้องกับ Generics ได้โดยไม่เกิดข้อผิดพลาด "subtype of constraint" หรือไม่?
ฉันต้องการใช้การผสมแบบ Required/Pick Generic เพื่อกำหนดว่า property (name) บน Cat/Dog มีอยู่หรือไม่ จากนั้นฉันต้องการสร้างฟังก์ชัน type guard ที่กำหนดว่า Cat/Dog ที่กำหนดมี property นั้นหรือไม่
อย่างไรก็ตาม ฉันได้รับข้อผิดพลาดดังต่อไปนี้บน NamedAnimal ในประเภทการส่งค่ากลับของฟังก์ชัน
มีวิธีใดในการแสดงแนวคิดด้านล่างใน TypeScript ของการรวม Generic แบบ Required/Pick กับตัวตรวจสอบชนิด (type guard) โดยไม่เกิดข้อผิดพลาดหรือไม่?
โค้ดที่ลอง:
export type AugmentedRequired<T extends object, K extends keyof T = keyof T> = Omit<T, K> &
Required<Pick<T, K>>;
type Cat = { name?: boolean };
type Dog = { name?: boolean };
type Animal = Cat | Dog;
type NamedAnimal<T extends Animal = Animal> = AugmentedRequired<T, 'name'>;
export function isNamedAnimal<T extends Animal = Animal>(animal: T): animal is NamedAnimal<T> { // Error is on NamedAnimal<T> in this line
return 'name' in animal;
}
ข้อผิดพลาดในโค้ดนี้:
A type predicate's type must be assignable to its parameter's type.
Type 'NamedAnimal<T>' is not assignable to type 'T'.
'NamedAnimal<T>' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Animal'.
ถ้านี่เป็นแนวคิดที่ทราบว่าไม่รองรับ มีวิธีที่ค่อนข้างสะอาดในการระงับข้อผิดพลาดหรือไม่?
หมายเหตุเพิ่มเติม:
วิธีหนึ่งที่ฉันพบว่าทำได้คือการใช้ฟังก์ชันแบบ inline ธรรมดาที่ไม่มีการตรวจสอบชนิดแบบ type guard ดังตัวอย่างด้านล่าง แต่ดูเหมือนว่าฉันควรจะสามารถกำหนดแนวคิดนี้อย่างชัดเจนได้
export const isNamedAnimal = (animal: Animal) => 'name' in animal;
คำตอบ #1
เพื่อให้ตัวบ่งชี้ชนิดในรูปแบบ t is U ได้รับอนุญาตให้ใช้เป็นชนิดผลลัพธ์ TypeScript ต้องสามารถเห็นว่า U สามารถกำหนดค่าให้กับ typeof t ได้ในตอนแรก นั่นคือ U ต้องถือเป็นการ ลดรูป ของ t หรือว่า U extends typeof T หาก Type เป็นชนิดที่ไม่เกี่ยวข้อง หรือชนิดที่กว้างกว่า หรือชนิดที่ซับซ้อนเกินไปสำหรับ TypeScript ที่จะมองว่าเป็นชนิดที่แคบลง คุณจะได้รับข้อผิดพลาด
คุณมีประเภททั่วไป T และหวังว่า TypeScript จะเห็นว่า Omit<T, "name"> & Required<Pick<T, "name">> เป็นการจำกัดของ T สิ่งนี้จะไม่เกิดขึ้น ประเภทยูทิลิตี้ Omit ถูกนิยามโดยใช้ประเภทยูทิลิตี้ Exclude ซึ่งถูกนิยามเป็นประเภทเงื่อนไข และแม้ว่า TypeScript จะสามารถประเมิน Omit<T, "name"> สำหรับ T เฉพาะ ได้ แต่ไม่สามารถทำเช่นนั้นสำหรับ T ทั่วไป หาก T เป็นประเภททั่วไป TypeScript ก็จะปล่อยให้ Omit<T, "name"> เป็นเหมือนไม่ถูกประเมินจริงๆ มันถูก เลื่อนออกไป ความตั้งใจของคุณคือ Omit<T, "name"> & Required<Pick<T, "name">> กำลังทำงานเหมือนกับการ "ผ่าตัด" บน T ซึ่งเมื่อผ่าตัดเสร็จสิ้น จะได้ผลลัพธ์ที่สามารถกำหนดค่าให้กับ T ได้ แต่ TypeScript ไม่สามารถมองเห็นความตั้งใจนั้น ซึ่งถือว่าเป็นฟีเจอร์ที่ขาดหายไปของ TypeScript อธิบายไว้ที่ microsoft/TypeScript#28884 อาจจะมีบางเวอร์ชันในอนาคตของ TypeScript ที่รองรับสิ่งนี้ แต่ตอนนี้ยังไม่เกิดขึ้น ดังนั้นคุณจึงได้รับข้อผิดพลาดนั้น
ทุกครั้งที่คุณเจอสถานการณ์ที่ TypeScript ล้มเหลวในการมองว่า U extends T แต่คุณมั่นใจว่ามันเป็นจริง คุณมักจะสามารถแทนที่ U ด้วยบางสิ่งบางอย่างที่ TypeScript เห็นว่า เป็นการขยาย T และซึ่งจะประเมินค่าเป็น U หากคุณถูกต้อง ตัวอย่างเช่น การตัดกัน T & U แน่นอนว่าสามารถมอบให้กับ T และถ้า U extends T เป็นจริง ดังนั้น T & U ก็ควรเทียบเท่ากับ U หรือคุณสามารถใช้ type ยูทิลิตี้ Extract เช่น Extract<U, T> ดังนั้นคุณสามารถทำให้โค้ดที่มีอยู่ของคุณคอมไพล์ได้หากคุณเปลี่ยนเป็น
declare function isNamedAnimal<T extends Animal>(
animal: T): animal is T & NamedAnimal<T>; // okay
หรือ
declare function isNamedAnimal<T extends Animal>(
animal: T): animal is Extract<NamedAnimal<T>, T>; // okay
แต่ในกรณีของคุณ คุณแทบไม่จำเป็นต้องผ่านความยุ่งยากนี้เลย เพราะแทบจะไม่จำเป็นต้องใช้ Omit ตั้งแต่แรก ประเภท T & Required<Pick<T, "name">> จะเหมือนกับ Omit<T, "name"> & Required<Pick<T, "name">> โดยพื้นฐาน การตัดกันของทรัพย์สินที่จำเป็นและทรัพย์สินที่เป็นทางเลือกคือทรัพย์สินที่จำเป็น ดังนั้นจึงไม่จำเป็นต้อง ลบ ทรัพย์สินที่อาจเป็นทางเลือกก่อนการตัดกัน แท้จริงแล้ว T & NamedAnimal<T> ที่ทำให้ทุกอย่างทำงานได้ข้างต้น สามารถมองว่าเป็น T & Omit<T, "name"> & Required<Pick<T, "name">> และถ้านั่นเหมือนกับ NamedAnimal<T> คุณก็สามารถใช้แบบนั้นเลย และ Omit<T, "name"> จะถูกรวมโดยการตัดกัน
ดังนั้น คุณสามารถเปลี่ยนโค้ดของคุณเป็นแบบนี้ได้:
type AugmentedRequired<T extends object, K extends keyof T = keyof T> =
T & Required<Pick<T, K>>;
และโค้ดต้นฉบับของคุณทำงานได้ตามเดิม:
declare function isNamedAnimal<T extends Animal>(
animal: T): animal is NamedAnimal<T>; // okay
ลิงก์ Playground ไปยังโค้ด
คำตอบ #2
ขอบคุณสำหรับความคิดเห็นด้านบน โดยเฉพาะจาก @jcalz ฉันได้ทำให้โค้ดของฉันง่ายขึ้นโดยการลบ Omit<T, K> และใช้แค่ T แทน ซึ่งทำให้ข้อผิดพลาดหายไปในขณะที่ยังคงความตั้งใจเดิมอยู่
export type AugmentedRequired<T extends object, K extends keyof T = keyof T> = T &
Required<Pick<T, K>>;
type Cat = { name?: boolean };
type Dog = { name?: boolean };
type Animal = Cat | Dog;
type NamedAnimal<T extends Animal = Animal> = AugmentedRequired<T, 'name'>;
export function isNamedAnimal<T extends Animal = Animal>(animal: T): animal is NamedAnimal<T> {
// Error is on NamedAnimal<T> in this line
return 'name' in animal;
}
