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

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 ให้ไม่เป็นแบบเลือกทั้งหมด แล้วค่อยปรับกลับเมื่อคุณทำเสร็จ

โอเค หวังว่านี่จะช่วยได้ โชคดีนะ!

ลิงก์ไปยังโค้ด