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

มีวิธีใดที่จะสื่อสาร 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;
}