דילוג לתוכן
  • דף הבית
  • קטגוריות
  • פוסטים אחרונים
  • משתמשים
  • חיפוש
  • חוקי הפורום
כיווץ
תחומים

תחומים - פורום חרדי מקצועי

💡 רוצה לזכור קריאת שמע בזמן? לחץ כאן!
  1. דף הבית
  2. תכנות
  3. TS: הגדרת interface לאובייקט המכיל מופעים של קלאס גנרי

TS: הגדרת interface לאובייקט המכיל מופעים של קלאס גנרי

מתוזמן נעוץ נעול הועבר תכנות
12 פוסטים 3 כותבים 318 צפיות
  • מהישן לחדש
  • מהחדש לישן
  • הכי הרבה הצבעות
התחברו כדי לפרסם תגובה
נושא זה נמחק. רק משתמשים עם הרשאות מתאימות יוכלו לצפות בו.
  • מוטי אורןמ מנותק
    מוטי אורןמ מנותק
    מוטי אורן
    כתב ב נערך לאחרונה על ידי
    #1

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

    כעת השאלה שלי היא כזאת:

    יש בידי מחלקה גנרית בשם Instruction, שמקבלת Type גנרי שיכול להיות או UnaryOperands או BinaryOperands - מתוך האינטרפייסים הבאים:

    interface UnaryOperands {
      dst: number;
    }
    interface BinaryOperands extends UnaryOperands {
      src: number;
    }
    

    קיימת מחלקה נוספת הנקראת בשם Processor, שמחזיקה property שנקרא instructions המחזיק אובייקט של מופעים של המחלקה הגנרית Instruction.

    שאלתי היא - מהי הדרך המומלצת להגדיר את ה-Type של האובייקט הנ"ל.

    חשבתי בתחילה להשתמש ב-Union type ככה:

    interface Instructions  {
      [index: string]: Instruction<UnaryOperands | BinaryOperands>;
    }
    

    אך התברר לי שזאת דרך בעייתית, לאחר שכתבתי מתודה במחלקה Processor שמקבלת instruction מתוך האובייקט instructions שב-Processor. את המתודה יישמתי כמו הדרכתו של @רפאל בצורה של Overloading כך:

    private perform (instruction: Instruction<{ dst: number }>): void;
    private perform (instruction: Instruction<{ dst: number, src: number }): void;
    private perform (instruction: Instruction<{dst: number, src?: number }): void { } // implementation
    

    אבל כשהשתמשתי במתודה הזאת והעברתי לה instruction, מיד TS הקפיץ לי את השגיאה הבאה:

    Argument of type 'Instruction<UnaryOperands | BinaryOperands>' is not assignable to parameter of type 'Instruction<{ dst: number; src: number; }>'.
          Type 'UnaryOperands | BinaryOperands' is not assignable to type '{ dst: number; src: number; }'.
            Property 'src' is missing in type 'UnaryOperands' but required in type '{ dst: number; src: number; }'
    

    המסקנה שהגעתי אליה היא (ייתכן שאני טועה) - שההגדרה שביצעתי ל-Type של Instructions היא לא נכונה, וצריכה להיות דרך יותר טובה להגדיר את ה-Type שלו.

    אשמח לעזרתכם.

    רפאלר yossizY 2 תגובות תגובה אחרונה
    1
    • רפאלר מנותק
      רפאלר מנותק
      רפאל
      השיב למוטי אורן ב נערך לאחרונה על ידי רפאל
      #2

      נאלצתי לנחש חלק מהקוד. השורות דלהלן מתקמפלות כראוי.

      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 } });
      
      מוטי אורןמ תגובה 1 תגובה אחרונה
      3
      • מוטי אורןמ מנותק
        מוטי אורןמ מנותק
        מוטי אורן
        השיב לרפאל ב נערך לאחרונה על ידי
        #3

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

        @רפאל אמר בTS: הגדרת interface לאובייקט המכיל מופעים של קלאס גנרי:

        נאלצתי לנחש חלק מהקוד

        מתנצל... עזרת לי מאוד.

        רפאלר תגובה 1 תגובה אחרונה
        2
        • רפאלר מנותק
          רפאלר מנותק
          רפאל
          השיב למוטי אורן ב נערך לאחרונה על ידי רפאל
          #4

          @מוטי-אורן אתה יכול להשתמש ב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;
          }
          
          תגובה 1 תגובה אחרונה
          3
          • yossizY מנותק
            yossizY מנותק
            yossiz
            השיב למוטי אורן ב נערך לאחרונה על ידי
            #5

            @מוטי-אורן רציתי להבין את הבעיה והפתרון, אבל נתקעתי כי לא הצלחתי לשחזר את הבעיה

            הנה playground שלא משחזר את הבעיה 😕
            האם תוכל ליצור אחד שכן משחזר?

            📧 יוסי@מייל.קום | 🌎 בלוג | ☕ קפה

            מוטי אורןמ תגובה 1 תגובה אחרונה
            1
            • מוטי אורןמ מנותק
              מוטי אורןמ מנותק
              מוטי אורן
              השיב לyossiz ב נערך לאחרונה על ידי
              #6

              @yossiz האמת שבבואי כעת לשחזר את הבעיה, אני קולט את דברי @רפאל שבאמת לא פרטתי מספיק את הקוד שהשתמשתי בו. השמטתי בשאלתי את הפרמטר השני שפונקציית ה-perform מקבלת, משום שסברתי שאינה קשורה לבעיה המדוברת.. כעת מסתבר לי שזוהי סיבת הבעיה.

              להלן הקישור ל-playground המשחזר את הבעיה.

              yossizY רפאלר 2 תגובות תגובה אחרונה
              1
              • yossizY מנותק
                yossizY מנותק
                yossiz
                השיב למוטי אורן ב נערך לאחרונה על ידי
                #7

                @מוטי-אורן נראה לי שכעת זכיתי להבין את הבעיה, עכשיו אני מתקשה בהבנת הפתרון...
                בנוסח שלי, הבעיה היא כזאת:
                הגדרת שתי "חתימות" לפונקציה שלך דרך function overloading. עכשיו הקריאה חייבת להתאים לאחת מהם, והיא לא מתאימה לשום אחד. כי הראשון אומנם מקבל בארגומנט הראשון <Instruction<UnaryOperands (וממילא גם כל דבר שיורש ממנו) אבל בשני הוא מצפה רק ל-dst. והשני מצפה לקבל <Instruction<BinaryOperands ו-Instruction<UnaryOperands | BinaryOperands> לא מתאים לו.

                לא הבנתי איך הפתרון פותר כלום.
                ובנסיונות שלי זה באמת לא פותר את הבעיה.

                מה שכן פותר הוא להוריד את החתימות של ה-overloads ולהשאיר רק את החתימה של ה-implementation, אבל אז ירד לך חלק מה-type safety

                📧 יוסי@מייל.קום | 🌎 בלוג | ☕ קפה

                תגובה 1 תגובה אחרונה
                2
                • רפאלר מנותק
                  רפאלר מנותק
                  רפאל
                  השיב למוטי אורן ב נערך לאחרונה על ידי רפאל
                  #8

                  להוסיף על דבריו הנכונים של @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).

                  yossizY תגובה 1 תגובה אחרונה
                  3
                  • yossizY מנותק
                    yossizY מנותק
                    yossiz
                    השיב לרפאל ב נערך לאחרונה על ידי yossiz
                    #9

                    @רפאל אמר ב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)
                    

                    הראשון תקין. למה? איפה מתועד התנהגות זו?

                    📧 יוסי@מייל.קום | 🌎 בלוג | ☕ קפה

                    רפאלר yossizY 3 תגובות תגובה אחרונה
                    2
                    • רפאלר מנותק
                      רפאלר מנותק
                      רפאל
                      השיב לyossiz ב נערך לאחרונה על ידי
                      #10
                      פוסט זה נמחק!
                      תגובה 1 תגובה אחרונה
                      0
                      • רפאלר מנותק
                        רפאלר מנותק
                        רפאל
                        השיב לyossiz ב נערך לאחרונה על ידי
                        #11

                        @yossiz אמר בTS: הגדרת interface לאובייקט המכיל מופעים של קלאס גנרי:

                        לכן התבלבלתי.

                        אתה ממש לא. התעלמתי לגמרי מהירושה. אתה פשוט צודק.

                        תגובה 1 תגובה אחרונה
                        2
                        • yossizY מנותק
                          yossizY מנותק
                          yossiz
                          השיב לyossiz ב נערך לאחרונה על ידי yossiz
                          #12

                          @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 על ידי ההשמה.

                          📧 יוסי@מייל.קום | 🌎 בלוג | ☕ קפה

                          תגובה 1 תגובה אחרונה
                          4

                          בא תתחבר לדף היומי!
                          • התחברות

                          • אין לך חשבון עדיין? הרשמה

                          • התחברו או הירשמו כדי לחפש.
                          • פוסט ראשון
                            פוסט אחרון
                          0
                          • דף הבית
                          • קטגוריות
                          • פוסטים אחרונים
                          • משתמשים
                          • חיפוש
                          • חוקי הפורום