סוגי איחוד - 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.
