סוגי איחוד - TypeScript - העבר אל פונקציה אלמנט אחד של UnionType אבל לא את שניהם


יש לי שני ממשקים ואני רוצה ליצור פונקציה שבה ייכלל או ממשק A או ממשק B אבל לא שניהם בו זמנית. שימוש ב-UnionType פשוט לא עובד מכיוון שהוא גם מאפשר ששני הממשקים ייכללו.

דוגמה (https://stackblitz.com/edit/typescript-qgytsy)

interface A{
  a: string;
}

interface B{
  b: string;
}

function aOrB(modifier: A | B) {}


aOrB({ a: '' }); // works!
aOrB({ b: '' }); // works!
aOrB({ a: '',  b: '' }); // also works but should not work!

האם יש דרך להשיג זאת, כך שהפנייה הראשונה והשנייה ל-aOrB(...) יעבדו אך האחרונה לא?

בברכה, בנדיקט

תשובה #1

אתה יכול להגדיר סוג שמוציא תכונות מסוימות:

type IFoo = {
  bar: string; can?: never
} | {
    bar?: never; can: number
  };


let val0: IFoo = { bar: "hello" } // OK only bar
let val1: IFoo = { can: 22 } // OK only can
let val2: IFoo = { bar: "hello",  can: 22 } // Error foo and can
let val3: IFoo = {  } // Error neither foo or can

אז השתמש בסוג הזה עבור פרמטר המודיפייר.

תשובה #2

מערכת הסוגים של TS היא מבנית, כלומר {a: 'a', b: 'b'} היא חברה תקינה של A | B משום שהיא ניתנת להקצאה ל-A כי היא כוללת את כל התכונות של A וניתנת להקצאה ל-B מאותה סיבה. אין בעיה להשתמש באובייקט כזה במקום שבו נדרש A או B שכן הוא כולל את כל מה ששניהם צריכים מבחינת מבנה. שקול את הקוד הבא:

function fOnA(modifier: A) { } // we require only A
const a = { a: '',  b: '' } // we create object with additional props
fOnA(a) // no error

אנחנו יכולים לבצע בדיקת סוג של סוג { a: '', b: '' }.

type ExtendsA = { a: '', b: '' } extends A ? true : false // evaluates true
type ExtendsB = { a: '',  b: '' } extends B ? true : false // evaluates true

כפי שאתם יכולים לראות { a: '', b: '' } הוא אובייקט חוקי שניתן להשתמש בו עבור A ועבור B.

למען הבהירות, שימוש במבנה כזה עם תכונות נוספות אינו מזיק, שכן תמיד יש לנו את מה שאנחנו צריכים.

המקרה שבו אנחנו רוצים להבחין אם יש לנו A או B הוא מקרה שבו עלינו ליצור דיסקרימיננט כדי שנוכל לבדוק איזה סוג של אובייקט נכנס לפונקציה. שקול:

interface A{
  kind: 'A', // discriminant 
  a: string;
}

interface B{
  kind: 'B', // discriminant 
  b: string;
}

function aOrB(modifier: A | B) {}

aOrB({ kind: 'A', a: '' }); // works!
aOrB({ kind: 'B', b: '' }); // works!
aOrB({ kind: 'B', a: '' }); // error as it should be
aOrB({ kind: 'B', b: '', a: '' }); // error as it should be

הוספתי את kind על מנת להבדיל בין שתי הגרסאות. גישה כזו יש לה יתרונות רבים, מכיוון שעכשיו אנחנו יכולים לבדוק בקלות אם יש לנו A או B פשוט על ידי בדיקת ה-kind.

פתרון נוסף הוא לחסום תכונות שאיננו רוצים על ידי מילת המפתח never. שקול:

interface A{
  a: string;
  b?: never;
}

interface B{
  b: string;
  a?: never;
}

function aOrB(modifier: A | B) {}

aOrB({ a: '' }); // works!
aOrB({ b: '' }); // works!
aOrB({ a: '', b: ''}); // error as it should be

על ידי הוספת prop?: never; אני נמנע מלתת לאובייקט לעבור את דרישות A ו-B.