TS: הגדרת interface לאובייקט המכיל מופעים של קלאס גנרי
-
נאלצתי לנחש חלק מהקוד. השורות דלהלן מתקמפלות כראוי.
type OptionalExceptFor<T, TRequired extends keyof T> = Partial<T> & Pick<T, TRequired> interface UnaryOperands { dst: number; } interface BinaryOperands extends UnaryOperands { src: number; } interface Instruction<T extends UnaryOperands> { operands: T; } function perform(instruction: Instruction<UnaryOperands>): void function perform(instruction: Instruction<BinaryOperands>): void function perform(instruction: Instruction<OptionalExceptFor<BinaryOperands, 'dst'>>): void { } perform({ operands: { dst: 45 } });
-
@מוטי-אורן אתה יכול להשתמש בUtility Type דלהלן כדי להפוך את כל השדות החסרים בBase לOptional:
/** * Construct a type with the properties of TOptional in which all keys not included in TRequired are marked as optional */ type MarkBaseMissingPropertiesAsOptional<TRequired, TOptional extends TRequired> = Partial<Exclude<TOptional, keyof TRequired>> & Pick<TOptional, keyof TRequired>;
שימוש
MarkBaseMissingPropertiesAsOptional<UnaryOperands, BinaryOperands>
תוצאה
{ dst: string; src?: string; }
-
@מוטי-אורן רציתי להבין את הבעיה והפתרון, אבל נתקעתי כי לא הצלחתי לשחזר את הבעיה
הנה playground שלא משחזר את הבעיה
האם תוכל ליצור אחד שכן משחזר? -
@yossiz האמת שבבואי כעת לשחזר את הבעיה, אני קולט את דברי @רפאל שבאמת לא פרטתי מספיק את הקוד שהשתמשתי בו. השמטתי בשאלתי את הפרמטר השני שפונקציית ה-perform מקבלת, משום שסברתי שאינה קשורה לבעיה המדוברת.. כעת מסתבר לי שזוהי סיבת הבעיה.
להלן הקישור ל-playground המשחזר את הבעיה.
-
@מוטי-אורן נראה לי שכעת זכיתי להבין את הבעיה, עכשיו אני מתקשה בהבנת הפתרון...
בנוסח שלי, הבעיה היא כזאת:
הגדרת שתי "חתימות" לפונקציה שלך דרך function overloading. עכשיו הקריאה חייבת להתאים לאחת מהם, והיא לא מתאימה לשום אחד. כי הראשון אומנם מקבל בארגומנט הראשון<Instruction<UnaryOperands
(וממילא גם כל דבר שיורש ממנו) אבל בשני הוא מצפה רק ל-dst
. והשני מצפה לקבל<Instruction<BinaryOperands
ו-Instruction<UnaryOperands | BinaryOperands>
לא מתאים לו.לא הבנתי איך הפתרון פותר כלום.
ובנסיונות שלי זה באמת לא פותר את הבעיה.מה שכן פותר הוא להוריד את החתימות של ה-overloads ולהשאיר רק את החתימה של ה-implementation, אבל אז ירד לך חלק מה-type safety
-
להוסיף על דבריו הנכונים של @yossiz:
נתונים שתי חתימות:
perform(instruction: Instruction<UnaryOperands>): void; perform(instruction: Instruction<BinaryOperands>): void;
החתימה הנכונה תיקבע לפי זהות הפרמטר הניתן לInstruction (הסוג של T), כיוון שהסוג שמועבר בדוגמה לInstruction הוא מסוג Union פתוח של שני הסוגים (
UnaryOperands | BinaryOperands
) המהדר אינו יכול לקבוע את זהות החתימה הנכונה משום שבזמן הכתיבה לא ניתן לדעת את הנתון הזה (זהות הסוג הספיציפי בתוך הUnion).די ברור מדוע הקוד דלהלן אינו תקין:
interface A { field1: number; } interface B { field2: number; } function perform(param: A): void; function perform(param: B): void; function perform(param: A | B): void { } let param: A | B; perform(param);
המהדר יעבור חתימה אחר חתימה בנסיון למצוא את החתימה המתאימה, בסיום אם נכשל יספק אינדיקציה עבור כל חתימה מדוע אינה מתאימה:
No overload matches this call. Overload 1 of 2, '(param: A): void', gave the following error. Argument of type 'A | B' is not assignable to parameter of type 'A'. Property 'field1' is missing in type 'B' but required in type 'A'. Overload 2 of 2, '(param: B): void', gave the following error. Argument of type 'A | B' is not assignable to parameter of type 'B'. Property 'field2' is missing in type 'A' but required in type 'B'.ts(2769)
לסיכום
הפיצרים Overloading וUnion types קצת בעייתיים לשימוש בו זמני, משום שעבור השימוש בOverloading דרוש סוג קונקרטי (אלא אם כן החתימה בעצמה מצהירה על Union). -
@רפאל אמר בTS: הגדרת interface לאובייקט המכיל מופעים של קלאס גנרי:
די ברור מדוע הקוד דלהלן אינו תקין:
interface A { field1: number; } interface B { field2: number; } function perform(param: A): void; function perform(param: B): void; function perform(param: A | B): void { } let param: A | B; perform(param);
כן. פה זה יותר ברור.
במקרה של @מוטי-אורן לפי איך שהוא הציג את השאלה בהתחלה זה באמת היה אמור לעבוד. כי אצלו ה-interface B
יורש מ-interface A
ואם כן ה-union שלהם מתאים באמת לחתימה הראשונה כי ה-B
הוא גםA
. לכן התבלבלתי.השאלה שלו הציגה מקרה כזו:
interface A { field1: number; } interface B extends A { field2: number; } function perform(param: A): void; function perform(param: B): void; function perform(param: A | B): void { } let param = { field1: 1 } as A | B; perform(param);
אגב, מעניין לראות מתוך דוגמאות אלו איזה מהם תקין:
interface A { field1: number; } interface B { field2: number; } function perform(param: A): void; function perform(param: B): void; function perform(param: A | B): void { } function getAOrB() : A | B { return { field1: 1 } } const param1: A | B = { field1: 1 } const param2 = { field1: 1 } as A | B const param3: A | B = getAOrB() const param4: A | B = { field1: 1, field2: 2 } perform(param1) perform(param2) perform(param3) perform(param4)
הראשון תקין. למה? איפה מתועד התנהגות זו?
-
@yossiz אמר בTS: הגדרת interface לאובייקט המכיל מופעים של קלאס גנרי:
הראשון תקין. למה? איפה מתועד התנהגות זו?
נראה לי שהבנתי, יש שתי רמות של טייפים ב-TS, יש את ה-declared type וה-observed type. ה-declared type משפיע על ה-assignability וה-observed type משפיע על השימוש בפועל בערך.
השמה של ערך לתוך משתנה יכול להשפיע על ה-observed type על ידי מנגנון שנקרא type narrowing.
דוגמה:// declared type let x: string | number // observed type becomes number (type narrowing by assignment) x = 1.0 // legal to use as number x.toFixed() // still fine to reassign to string x = "Hi" // observed type is now string (again type narrowing by assignment) x.charAt(1); // coerced to string | number x = x as string | number // illegal! // Property 'charAt' does not exist on type 'string | number'. // Property 'charAt' does not exist on type 'number'.(2339) x.charAt(1)
מקור:
https://www.typescriptlang.org/docs/handbook/2/narrowing.html#assignmentsלכן בשורה זו:
const param1: A | B = { field1: 1 }
למרות שה-declared type הוא
A | B
אבל יש מיד type narrowing על ידי ההשמה.