Typescript: วิธีการยกเว้นอินเทอร์เฟซที่มีเฉพาะคุณสมบัติเชิงเลือกออกจากชนิดรวม
ฉันมีอินเทอร์เฟซไม่กี่ตัวที่คล้ายกับเหล่านี้ โดยอินเทอร์เฟซที่ฉันต้องการยกเว้นในภายหลังประกอบด้วยเฉพาะคุณสมบัติที่เป็นทางเลือกเท่านั้น:
interface P {
id: string
}
interface A extends P {
attrA: string
}
interface B extends P {
attrB?: string
}
ตอนนี้ การยกเว้นอินเทอร์เฟซนี้จากการรวมกับอินเทอร์เฟซอื่น ๆ ไม่ทำงาน:
type R = Exclude<A | B, B> // expected: A, is: never
ประเภท R คือ never แต่ควรจะเป็น A.
ฉันเห็นว่า Exclude<T, U>=T extends U ? never : T และที่ชัดเจนคือ A extends B จะเป็น true ถ้า B ประกอบไปด้วยเฉพาะคุณสมบัติที่เป็นตัวเลือก ทำไมถึงเป็นแบบนั้น?
และฉันจะสามารถยกเว้นชนิดหนึ่งจากชนิดยูเนียนของอินเทอร์เฟซได้อย่างถูกต้องอย่างไร?
คำตอบ #1
เมื่อพิจารณาประเภทเหล่านี้:
interface P {
id: string
}
interface A extends P {
attrA: string
}
interface B extends P {
attrB?: string
}
type R = Exclude<A | B, B>; // never
ชนิด R เป็น never เพราะ A extends B ถูกมองโดยคอมไพเลอร์ว่าเป็นจริง:
const a: A = { id: "a", attrA: "A" };
const b: B = a; // okay
คุณสามารถเห็นได้ว่า a เป็น A ที่ถูกต้อง แต่ก็ยังเป็น B ที่ถูกต้องเช่นกัน ประเภทของวัตถุใน TypeScript ไม่ใช่แบบแม่นยำ คุณสามารถขยายประเภทได้โดยการเพิ่มคุณสมบัติ (ซึ่งเป็นเหตุผลที่ A สามารถกำหนดค่าให้ P ได้ แม้ว่า A จะมีคุณสมบัติเพิ่มเติม) จากมุมมองของระบบประเภท ทุกค่าของประเภท A ก็เป็นค่าของประเภท B ด้วย ดังนั้น Exclude<A | B, B> จะลบทั้ง A และ B ออกจากการรวม และคุณจะเหลือเพียง never
แน่นอนว่า จริง ๆ แล้วการที่ A สามารถกำหนดค่าให้กับ B ได้นั้นไม่ปลอดภัยประเภท (type safe) ตัวคอมไพเลอร์ถือว่า ทุก ค่าของประเภท A เป็นประเภท B ด้วย แต่จริง ๆ แล้วมันเหมือนกับ เกือบทุก ค่ามากกว่า ยังมีค่าบางอย่างของประเภท A ที่ไม่ควรกำหนดค่าให้กับประเภท B ที่มีประเภทไม่เข้ากัน:
const aButNotB = {id: "a", attrA: "A", attrB: 123}
const alsoA: A = aButNotB; // okay
const notB: B = aButNotB; // error, attrB is incompatible
const butWait: B = alsoA; // no error?! attrB is still incompatible, but compiler forgot!
แต่เนื่องจาก TypeScript ยังไม่รองรับประเภทที่ปฏิเสธ จึงไม่มีวิธีใน TypeScript ที่จะแสดง "ทุก A ที่ไม่ใช่ B" เป็นประเภทที่ชัดเจน ดังนั้นเมื่อเปรียบเทียบสองประเภทของวัตถุ ตัวคอมไพเลอร์จึงเพิกเฉยต่อคุณสมบัติตัวเลือกที่มีอยู่เพียงหนึ่งในนั้น... ทำให้เกิดจุดบอดนี้
นั่นคือเหตุผลว่าทำไมสิ่งนี้ถึงเกิดขึ้น สำหรับวิธีแก้ไขมัน ขึ้นอยู่กับกรณีการใช้งานของคุณ
ในอุดมคติ ถ้าคุณจำเป็นต้องสามารถแยกค่าของยูเนียนของชนิดต่าง ๆ ออกจากกันได้ คุณควรใช้ยูเนียนที่สามารถแยกแยะได้ ซึ่งหมายความว่ายูเนียนนั้นควรมีคุณสมบัติหรือหลายคุณสมบัติที่สามารถใช้บอกความแตกต่างระหว่างกันได้อย่างชัดเจน ตัวอย่างเช่น ลองจินตนาการการเพิ่มคุณสมบัติ type เพื่อแยกแยะ:
interface Aʹ extends A {
type: "A";
}
interface Bʹ extends B {
type: "B";
}
type Rʹ = Exclude<Aʹ | Bʹ, Bʹ>; // Aʹ
ในปัจจุบัน ยังไม่มีวิธีใดที่ค่าประเภท Aʹ จะถูกกำหนดให้กับประเภท Bʹ หรือในทางกลับกัน และตอนนี้ Exclude ทำงานได้อย่างที่คุณคาดหวัง
หรือถ้าคุณแค่กำลังดูวิธีการทำ Exclude ตามที่คุณคาดหวัง คุณสามารถปรับแก้ประเภท A และ B ของคุณก่อนที่จะประมวลผล จากนั้นจึงคืนค่าผลลัพธ์กลับเหมือนเดิม ดังนี้:
type OptToUndef<T> = {
[K in keyof Required<T>]: T[K] | ({} extends Pick<T, K> ? undefined : never)
};
type UndefToOpt<T> = (Partial<
Pick<T, { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T]>
> &
Pick<
T,
{ [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]
>) extends infer O
? { [K in keyof O]: O[K] }
: never;
พื้นฐานแล้ว OptToUndef<T> จะรับประเภทวัตถุ T และทำให้ทุกประเภทที่เป็นทางเลือกกลายเป็นประเภทที่จำเป็น แต่ประเภทของคุณสมบัติของมันจะรวม undefined ด้วย และ UndefToOpt<T> จะรับประเภทวัตถุ T และทำให้ทุกคุณสมบัติที่มีประเภทรวมถึง undefined กลายเป็นคุณสมบัติแบบทางเลือก ทั้งสองเป็นการกระทำที่ตรงข้ามกันโดยมาก (ตราบใดที่คุณไม่มีประเภท required ที่รวม undefined) จากนั้นคุณสามารถทำแบบนี้ได้:
type UA = OptToUndef<A>; // {id: string; attrA: string }
type UB = OptToUndef<B>; // {id: string; attrB: string | undefined }
type UR = Exclude<UA | UB, UB>; // same as UA
type Rfixed = UndefToOpt<UR>; // same as A
บางอย่างแบบนั้นอาจใช้ได้สำหรับคุณ โดยที่คุณปรับ B ให้ไม่เป็นแบบเลือกทั้งหมด แล้วค่อยปรับกลับเมื่อคุณทำเสร็จ
โอเค หวังว่านี่จะช่วยได้ โชคดีนะ!
ลิงก์ไปยังโค้ด
