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

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

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

צלילה לעומק TS: טייפ X לא זהה ל-union של כל הערכים האפשריים שהטייפ כולל

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

    בפוסט זה אני הולך לשתף דבר שלמדתי לאחרונה ב-TS שהיה חידוש עבורי
    אני משתמש בנושא זה כקרש קפיצה לכתוב מבוא כללי ל-TS. כדי להסביר את החידוש שגיליתי, אסביר בצורה מתומצתת את המושגים שצריכים להכיר כדי להבין את החידוש
    (אם מישהו יתחיל להשתמש בשפה בזכותי והיה זה שכרי. פשוט חבל שיש כל כך הרבה מתכנתים מוכשרים בקהילה שלנו שלא משתמשים ב-TS מחוסר היכרות...)

    מכיון שאני כותב את הפוסט בהמשכים אשמח אם התגובות יהיו בנושא נפרד

    התוכנית:

    • קורס מזורז ב-TS
    • קטעי הקוד המוקשים לכאורה
    • התשובה

    מקווה שיהיה קל לקריאה ולתועלת

    ובכן, אעביר אותך קורס מזורז מאוד ב-TS:

    מה זה טייפ

    דרך אחת להסתכל על טייפ הוא ככה:
    👈 טייפ הוא סט (set - כן, ההוא מתורת הקבוצות...) של ערכים.
    לדוגמה:

    • הטייפ number מייצג את הסט שכולל בתוכו את כל המספרים
    • הטייפ 1 מייצג סט שכולל בתוכו ערך אחד: המספר 1, כמו"כ כל ערך ב-JS הוא גם שם של טייפ שכולל חבר אחד - הערך הזה בעצמו
    • הטייפ { foo: string; bar: number } מייצג את הסט שמכיל כל האובייקטים האפשריים שמכילים שדה foo מסוג string ושדה bar מסוג number
    • הטייפ never מייצג סט ריק. סט בלי חברים.
    • הטייפ unknown מייצג את הסט של כל הערכים האפשריים. "סט סטי הסטים". הסט הכי גדול ביקום הטייפים. לכן אין הרבה מה לעשות עם ערך מסוג unknown כי אתה לא יודע איזה פעולות מותרים לעשות איתו.

    דרך נוספת לחשוב על טייפים:
    👈 הטייפ של הערך אומר לנו איזה פעולות מותרות על הערך
    למשל:

    • הטייפ number אומר לנו שמותר לעשות פעולת חיבור וחיסור על ערך זה עם מספרים אחרים
    • הטייפ string אומר לנו שמותר לנו לקרוא לפונקציה בשם startsWith על הערך שלנו
      וכן הלאה

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

    מתמטיקה עם טייפים:

    TS נותן שני כלים אריתמטיים לבניית טייפים מורכבים מטייפים אחרים:

    חיבור - UNION

    האופרטור | ב-TS עושה את פעולת החיבור באריתמטיקה של טייפים
    למשל:

    • טייפ 1 | 2 מייצג חיבור של שני הסטים 1 ו-2. התוצאה היא סט (טייפ) שכולל שני חברים, 1 ו-2
    • string | number מחבר את שני הסטים והתוצאה היא סט גדול שכולל בתוכו כל המחרוזות האפשריים וכל המספרים האפשריים
    • חיבור עם never (להזכירכם: זהו הסט עם 0 חברים) הוא כמו פעולת חיבור מספרי של מספר כלשהו עם 0. הפעולה לא משנה את הערך המקורי.

    חיסור - INTERSECTION

    למדנו איך להרחיב סט עם אופרטור | עכשיו נלמוד איך להצר סט עם אופרטור &.
    האופרטור & מקבל שני סטים משני צידי האופרטור ומחזיר את הסט שכולל רק את החברים בשני הסטים גם יחד
    למשל:

    • { foo: string } & { bar:number } לוקח את הסט שכולל כל האובייקטים האפשריים שיש להם שדה foo מסוג string, וכן את הסט שכולל כל האובייקטים האפשריים שיש להם שדה bar מסוג number, והוא מחזיר סט של כל האובייקטים האפשריים שחברים בשתי הקבוצות: דהיינו אובייקטים שיש להם גם שדה foo וגם שדה bar
    • פעולת & unknown לא עושה כלום כי כל ערך אפשרי נמצא בתוך סט ה-unknown

    תכנות עם טייפים

    TS נותן כלים יותר משוכללים לבניית טייפים על ידי "תכנות"

    פונקציות - GENERICS

    תחביר ה-generics מקביל לפונקציה המוכרת מתכנות רגיל.
    אבל בעוד שפונציקה רגילה פועלת על ערכים, ה-generic פועל על טייפים.
    זה פונקציה שמקבל כארגומנט טייפ, ופולט את הטייפ הסופי לפי הלוגיקה של ה-body של הפונקציה.
    לדוגמה:

    • פונקציה שמקבל נוסח תפילה ופולט את הטייפ של היהודי עם נוסח תפילה זו:
    // הצהרת טייפ שכולל כל נוסחאות התפילה האפשריים
    type Nusach = 'Ashkenaz' | 'Sefarad' | 'Edot Hamizrach'
    
    // הצהרת טייפ גנרי שמקבל נוסח תפילה ומחזיר טייפ של יהודי שמתפלל בנוסח הנ"ל
    type Jew<T extends Nusach> = {
      name: string
      nusach: T
    }
    
    // ערך עם שדה שם ושדה נוסח 
    const aJew = {
        name: 'Zundel',
        nusach: 'Ashkenaz'
    }
    
    // נסיון השמה למשתנה מסוג Jew<'Sefarad'>
    // TS מתריע על אי התאמה בין הטייפ לערך
    const test: Jew<'Sefarad'> = aJew
    

    השורה האחרונה תפלוט שגיאה, כי טייפ מסוג Jew<'Sefarad'> לא יכול להתפלל נוסח אשכנז

    מילת המפתח extends

    בדוגמה דלעיל נתקלנו לראשונה (בקורס הזה) במילת המפתח extends, ב-TS הכוונה של extends הוא (לפי צורת החשיבה שהצגנו בתחילת דברינו) שהטייפ שבצד שמאלי כולו חבר בסט שמיוצג על ידי הצד הימני.
    ובדוגמה הנ"ל: T extends Nusach הכוונה הוא שהפרמטר T רק מקבל טייפים שהם חברים בסט Nusach
    אם ננסה ליצור יהודי כזה: Jew<'foo'> נקבל שגיאה שהמחרוזת foo הוא לא חבר בקבוצת Nusach

    נפגוש שוב את המילה extends בהמשך

    תנאים

    התחביר לכתיבת תנאי ב-TS:

    T extends U ? V : W
    

    שימו לב לנקודות הבאות:
    בתכנות רגיל אנחנו מורגלים לבדיקת המון דברים בתנאים, וכן לשלוט על פעולות מגוונות על ידי תנאים אלו
    ב-TS לעומת זאת:

    • הפרט היחיד שאפשר לבדוק בתנאי הוא extends - כלומר, חברות של הצד השמאלי בצד הימני
    • הדבר היחיד שאתה יכול לתלות בתנאי הזה הוא הטייפ של התוצאה של הביטוי

    דוגמה לשימוש בתנאי ופונקציה ביחד:

    type LabelOf<T> = T extends { label: string } ? T['label'] : 'no name' 
    

    Index Signatures

    אפשר לתאר את ה-shape של אובייקט בלי לפרט בדיוק את השם של השדות, במקום זה כותבים את הטייפ של המפתחות והטייפ של הערכים
    לדוגמה:

    type X = {
      [key: string]: number
    }
    

    הכוונה בהצהרה זו הוא שמדובר באובייקט שיש בו מפתחות מסוג string וערכים מסוג number

    לולאות - Mapped Types

    TS לוקח את התחביר הזה שלב נוסף קדימה באמצעות mapped types
    בתחביר זה אפשר לעבור בלולאה על כל הערכים האפשריים הכלולים במפתח (key) ולקבוע את הטייפ של הערך עבור אותו מפתח לפי כל לוגיקה שתבחר
    דוגמה:

    type MyMap<T> = {
        [K in keyof T]: K extends `${string | undefined}foo${number}` ? string : number
    }
    
    type Test = MyMap<{ foo1: any, a_foo2: any, bar3: any }>
    

    keyof

    כאן אנחנו נתקלים לראשונה במילת המפתח keyof:
    זה אופרטור שמחזיר union של כל שמות השדות של האופרנד שלו
    למשל: keyof {foo:any, bar:any} שווה ל-'foo' | 'bar'

    ה"פונקציה" (יותר מקובל לקרוא לו generic type) MyMap מקבל פרמטר T (מכיון שלא הגבלנו אותו על ידי מילת המפתח extends - הוא יכול להיות מכל סוג שהוא), ומחזיר אובייקט שיש לו אותם שדות של האובייקט המקורי, אבל אם התבנית של שם השדה מתאים לתבנית מחרוזת אופציונלית + foo + מספרי כלשהו, אז הערך של השדה חייב להיות string, אחרת הערך חייב להיות number

    יותר מקובל ושימושי להשתמש ב-mapped types עם תנאי שבודק את הטייפ של ה_ערך_ של האובייקט המקורי במקום תנאי שבודק את הטייפ של ה_מפתח_ של האובייקט המקורי. עושים את זה על ידי אינדוקס ככה:

    type MyMap<T> = {
        [K in keyof T]: T[K] extends X ? Y : Z
    }
    

    נראה דוגמה שימושית לזה מיד בקטע הבא

    דוגמה אחרונה: Omit

    בוא נבדוק את ההבנה שלך על ידי הדוגמה הבאה.
    (ההגדרות Exclude ו-Omit מגיעים מובנים ב-TS)

    // type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }
    // type Exclude<T, U> = T extends U ? never : T
    
    type MyObject = {
        id: number
        foo: string
        bar: number
    }
    
    type MyObjectWithoutId = Omit<MyObject, 'id'>
    

    טוב, התעייפתי לבינתיים, אמשיך בעז"ה בהזדמנות הבאה...

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

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

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

      הודעות

      א. תודה לכל מי שטרח להשאיר לי משוב, אם בצורת הצבעה על הפוסט ואם בערוצים פרטיים (כמה חברים כתבו אלי בערוצים אחרים), אני מעריך את זה!
      ב. לכל מי שהתקשה להבין את הפוסט הקודם:
      אל תאשים את עצמך. זה לא היה פוסט קל להבנה! כתבתי דברים בקיצור, ולפעמים השמטתי לגמרי הסברים נחוצים.
      הנושא עצמו מאוד תיאורטי, וגם הדוגמאות לא היו ממש מעשיות.
      אני מתנצל. (אנסה להבא להביא דוגמאות יותר מעשיות)
      ולמי שלא התקשה כלל: או שיש לך הכירות קודמת עם הנושא, או שאתה מוכשר, או שלא הבנת באמת
      ג. חשוב להבהיר! אין שום צורך להכיר את כל התוכן התיאורטי שכתבתי כדי לקבל תועלת מ-TS. גם אם הנושא לא מושך אותך או אפילו דוחה אותך, אני עדיין ממליץ בחום להשתמש ב-TS כי אפשר לקבל רוב התועלת בלי להבין את הרקע התיאורטי. הפוסט שלי חשוב למי שרוצה ליצור טייפים. רוב הזמן אתה צורך טייפים שאחרים כתבו ולא יוצר טייפים חדשים. וגם רוב הטייפים שתיצור יהיו פשוטים בלי צורך בידע תיאורטי עמוק.
      ד. העירו לי על כמה דברים שלא הסברתי די הצורך בפוסט הקודם ואנסה להבהיר אותם כאן

      מה זה טייפ?

      (הרחבה של הקטע "מה זה טייפ" בפוסט הקודם)

      התפקיד של טייפים בשפות תכנות זהה לכל השפות: בהנתן ערך כלשהו, המתכנת רוצה לדעת בצורה סטטית (כלומר, בלי להריץ את התוכנה בפועל) איזה פעולות זמינים לו על ערך זה, איך מותר להשתמש בערך.
      אבל יש הבדל מהותי בין ההתעסקות עם טייפים בשפות סטטיות (statically typed) "קלאסיות" (JAVA, C++, C#, etc) לטייפים ב-TS
      כי בשפות קלאסיות אין עיסוק ישיר עם טייפים עצמם. עוסקים עם קלאסים, שהם "תבניות" או "אב טיפוס" לערכים, קלאס מגדיר איזה שדות הערך חייב להכיל, ובאיזה פעולות הוא צריך לתמוך. וכתוצאה נוצר "טייפ" (שהוא הסט של כל הערכים האפשריים שמתאימים לתבנית) אבל אין עיסוק ישיר עם טייפים עצמם
      ב-TS יש עיסוק ישיר עם טייפים עצמם. מחברים טייפים אחד לשני, מחסרים טייפים אחד מהשני, בונים טייפים מטייפים אחרים, דברים שלא קיימים בשפות קלאסיות.
      בשפה קלאסית אין מושג של literal type (הטייפ של ערך literal, סט בעל ערך יחיד). כי כל טייפ הוא תוצאה מהצהרה של קלאס שהוא תבנית להרבה ערכים כי העיקר של הטייפ הוא הפעולות שזמינים עליו.
      לא כן ב-TS שאפשר לעסוק ישירות עם המושג טייפ.
      לכן חשוב לחשוב על טייפים ב-TS כסטים, במקום כתבניות כמו בשפות קלאסיות

      GENERICS

      הדוגמה שהבאתי בפוסט הקודם לא היה מספיק ברור וגם לא כל כך מעשי
      אביא דוגמה מעשי ביותר
      נגיד שאתה בונה קליינט ל-API
      אתה יודע שה-API המדובר תמיד מחזיר תשובות בתבנית זה:

      type ApiResponse = {
        success: boolean
        data: object | undefined
        error: string | undefined
      }
      

      השימוש ככה:

      type User = { ... }
      type Transaction = { ... } 
      
      class ApiClient {
        // users
        getUserById(id: number): ApiResponse { ... }
        updateUser(newUser: User): ApiResponse { ... }
        // transactions
        listTransactions(userId: number): ApiResponse { ... }
        // ...
      }
      
      const apiClient = new ApiClient
      
      const res: ApiResponse = apiClient.getUserById(id)
      
      if (res.success) {
        const user = res.data
        // do something with user
      } else {
        console.error(error)
      }
      

      הבעיה בקוד הזה הוא שבשורה 18, הדבר היחיד ש-TS יודע על המשתנה user הוא שזה מסוג object אבל הוא לא יודע איזה סוג אובייקט.
      דרך אחת לטפל בזה הוא להצהיר על טייפ בשם UserApiResponse ושם להצהיר על הטייפ המלא של שדה data
      הבעיה היא שצריך לחזור שוב ושוב על הצהרה זו לכל סוג של XXXApiResponse
      אפשרות אחרת הוא להצהיר שהטייפ של res.data הוא User

      const user = res.data as User
      

      אבל בצורה זו מאבדים את הבטחת האבטחה של TS וצריך לסמוך על המתכנת שהצהיר נכון על הטייפ
      הפתרון הוא להשתמש ב-generic שהוא "טייפ בונה טייפים"
      ככה:

      type ApiResponse<T> = {
        success: boolean
        data: T | undefined
        error: string | undefined
      }
      
      type User = { ... }
      type Transaction = { ... } 
      
      class ApiClient {
        // users
        getUserById(id: number): ApiResponse<User> { ... }
        updateUser(newUser: User): ApiResponse<User> { ... }
        // transactions
        listTransactions(userId: number): ApiResponse<Transaction[]> { ... }
        // ...
      }
      
      const apiClient = new ApiClient
      
      const res: ApiResponse = apiClient.getUserById(id)
      
      if (res.success) {
        const user = res.data
        // user is type User
      } else {
        console.error(error)
      }
      

      בפסאודו קוד ההצהרה דומה לקוד כזה:

      const ApiResponse = function(T: type): type {
        return new Type(`{
          success: boolean
          data: ${T} | undefined
          error: string | undefined
        }`)
      }
      

      תנאים

      יש כמה בעיות בדוגמה שהבאתי למעלה עבור שימוש בתנאי
      א. זה לא כל כך מעשי
      ב. יש באג (הטייפ לא עושה מה שהתכוונתי שהוא יעשה - אולי אכתוב על זה בפוסט אחר) ואין לי ממש דרך לפתור את הבאג
      ג. השתמשתי בתחביר שלא הצגתי מקודם (אינדוקס)

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

      נגיד שה-API מחזיר לפעמים אובייקט יחיד ולפעמים מערך
      אם הוא מחזיר מערך אז יש שדות נוספות בתשובה
      אולי נגיד שדה count שמחזיר כמה אובייקטים יש בסה"כ
      איך נגדיר type כזה?

      type BaseApiResponse<T> = {
        success: boolean
        data: T | undefined
        error: string | undefined
      }
      
      type ApiResponse<T> = T extends any[] ? BaseApiResponse<T> & { count: number } : BaseApiResponse<T>
      

      אפשר לתרגם את זה לקוד פסאודו זה:

      function ApiResponse(T: type): type {
        const base = `{
          success: boolean
          data: ${T} | undefined
          error: string | undefined
        }`
        if (T instanceof Array(any)) {
          return `base & { count: number }`
        } else {
          return base
        }
      }
      

      Mapped Types

      הבאנו למעלה דוגמה שהיא גם מסובכת מדי וגם לא כל כך מעשי לשימוש יומיומי
      אני רוצה להביא דוגמה נורמלית מתוך הטייפים המובנים (TS מגיע עם "ספרייה סטנדרטית" של טייפים מובנים)
      נבנה על הדוגמה הקודמת שלנו של קליינט API
      אז אתה רוצה להגדיר שהפונקציה updateUser מקבל אובייקט מסוג User אבל בשונה מ-User, כל השדות הם אופציונליות. (כי מי שרוצה לעדכן רק שדה אחת לא צריך לשלוח את כל השדות)

      אז אפשר לכתוב טייפ חדש

      type User = {
        name: string
        foo: string
        bar: number
      }
      
      type UserUpdate = {
        name?: string
        foo?: string
        bar?: number 
      }
      

      אבל זה בזבוז נייר דיו וביטים, וכמובן זה לא DRY מה שאמור לזעזע כל מתכנת ששוה משהו

      הפתרון הוא:

        updateUser(newUser: Partial<User>): ApiResponse<User> { ... }
      

      השתמשנו ב-helper (כך נקראים "פקונציות" אלו שעוזרים לבניית טייפים מבוססים טייפים קיימים) בשם Partial שלוקח טייפ ומחזיר טייפ עם אותם שדות, רק שכולם נהיים אופציונליים
      איך עובד ה-helper הזה?
      נסתכל בקוד המקור:

      type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
      

      מילת המפתח in עובד ככה: עבור כל חבר ב-union שצד הימני מייצג, הוא מריץ את הביטוי שאחרי הנקדותיים כאשר המשתנה P מייצג את החבר הזה מה-union

      שימו לב לתחביר שלא הצגנו מקודם: T[P] - זה אינדוקס בטייפ שעובד בדיוק כמו אינדוקס אובייקט רגיל. T[P] הוא הטייפ של שדה P בתוך טייפ T

      נתרגם את זה לפסאודו קוד:

      function keyOf(o: object): union {
          return new Union(Object.keys(o))
      }
      
      function Partial(T: type): type {
          const res = {}
          for (const P in keyOf(T)) {
              res[P] = T[P] | undefined
          }
          return res
      }
      

      (סליחה. בפסאודו קוד הזה השתמשתי במוסכמות אחרות מהקטעים הקודמים. בקודמים יצרתי טייפ על ידי העברת מחרוזת ל"בנאי של הטייפ" - כביכול. הפעם לשם נוחות התייחסתי לטייפ כאובייקט. זה רק פסאודו קוד כמובן ואני מקווה שהכוונה ברורה)

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

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

        הטייפ השולל: never

        נחזור שוב ל-never לכמה רגעים
        הטייפ never הוא סט בלי חברים, טייפ ששום ערך לא יכול להיות בו חבר.
        נמצא שלא יקרה אף פעם שלערך אמיתי יהיה טייפ never
        אם כן לְמָה זה טייפ שימושי?

        שלילת מצב

        אחד מהשימושים של טייפ זה הוא לסמן ל-TS שמצב מסויים אמור להיות בלתי אפשרי, ולקבל אישור מ-TS שזה באמת בלתי אפשרי
        למשל: אם רוצים לוודא שב-switch עברנו על כל האפשרויות ולא פיספסנו אחד מהם בטעות אפשר לכתוב ככה:

        type Color = 'red' | 'blue' | 'green'
        
        function handleColor(color: Color) {
          switch (color) {
            case 'red':
              // ...
              break
            case 'blue':
              // ...
              break
            case 'green':
              // ...
              break
            default:
              throw new Error('Impossible!')
          }
        }
        

        פה אנחנו בודקים בזמן ריצה שלא הגענו לענף הבלתי אפשרי

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

        type Color = 'red' | 'blue' | 'green'
        
        function handleColor(color: Color) {
          switch (color) {
            case 'red':
              // ...
              break
            case 'blue':
              // ...
              break
            case 'green':
              // ...
              break
            default:
              const _test:never = color
              throw new Error('Impossible!')
          }
        }
        

        עיין שורה 15. אנחנו מצהירים ל-TS שבשלב הזה מיצינו את כל הערכים האפשריים ש-color יכול לקבל ופה הטייפ חייב להיות never, כלומר זה לא יקרה אף פעם
        (להבין יותר איך זה עובד, כדאי להכיר את המושג narrowing, בקיצור נמרץ: מנוע TS עוקב אחרי משתנים תוך כדי זרימת הקוד וכאשר הוא רואה ביטויים מסויימים בקוד, הוא מבין אותם ומוציא מתוך ה-union של הטייפ של המשתנים את אותם הטייפים שהקוד שולל, ובהקשר שלנו, בכל ענף של ה-switch, מנוע TS יכיר בעובדה שהטייפ של color הוא רק הערך שאותו ענף מטפל, ובענף default הטייפ הוא האפשרויות שה-switch לא מיצה)
        ברגע שנוסיף עוד אפשרות ל-union של Color נקבל מיד אזהרה שהמשתנה _test קיבל ערך שהטייפ שלו הוא לא never

        שלילת ערך

        אבל השימוש שמעניין אותנו יותר פה הוא השימוש השני,
        כאשר יוצרים טייפ חדש באמצעות ביטוי עם תנאי, ורוצים שענף אחד של התנאי לא יחזיר שום טייפ, או יותר נכון: יחזיר טייפ ריק, במקום כזה בא never לידי שימוש
        נדגים את זה על ידי ה-helper המובנה - Exclude
        (helper הוא פונקציה שעוזר לבניית טייפ שמבוסס על טייפ קיים. נזכיר: פונקציה ב-TS == ‏generic)
        הטייפ Exclude מגיע מובנה ב-TS ושימושו הוא כאשר רוצים ליצור מ-union קיים, טייפ שדומה למקור רק שחסר ממנו אחד או יותר ערכים.
        למשל:

        type Weekday = 'sunday' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday'
        type Workday = Exclude<Weekday, 'friday' | 'saturday'>
        

        כפי שאפשר לראות: ה"פונקציה" Exclude מקבלת כפרמטר ראשון את הטייפ המקורי ובפרמטר השני union של טייפים (או טייפ יחיד) לחסר מהטייפ המקורי.

        איך הוא עובד?
        הנה קוד המקור

        type Exclude<T, U> = T extends U ? never : T
        

        להבין את זה צריך להקדים עוד דבר אחד: ביטוי של תנאי שמריצים על union עובד על כל אבר של ה-union בנפרד
        בתיעוד של TS קוראים לזה: Distributive Conditional Types
        כלומר זה עובד כמו הקוד פסואודו הבא:

        function Conditional(source: Union, condition: Type => Bool, resultIfTrue: Type => Type, resultIfFalse: Type => Type) {
          const runConditional = (type: Type) => {
            if (condition(type)) {
              return resultIfTrue(type)
            } else {
              return resultIfFalse(type)
            }
          }
          return source.split('|').map(runConditional).join('|')
        }
        
        function Exclude(source: Union, exclusions: Union) {
          return Conditional(source, t => t.extends(exclusions), t => t, t => never)
        }
        
        Exclude(Weekday, 'friday' | 'saturday') // 'sunday' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | never | never
        

        מקווה שהפסאודו קוד מובן
        רק מזכיר שוב כמה נקודות שכבר הזכרנו:

        • בדיקת extends בודק עם הצד שמאלי כולו כלול בתוך הסט שהצד הימני מייצג
        • פעולת union עם never - כמוהו כמו אי-פעולה. הוספת סט ריק לסט המקורי לא מוסיף שום דבר, לכן בתוצאה של Exclude אפשר להשמיט את ה-never בלי שום השלכה

        הערה בענין מילת extends

        ℹ בדרך אגב, כמה אנשים שאלו שאלה זו: למה המצב שבו טייפ אחד כלול לגמרי בתוך טייפ אחר נקרא בלע"ז extends, הרי המשמעות של extend בלע"ז הוא "הרחבה" ופה מדובר על הצרה?!
        התשובה לזה לכאורה (וכן הסכים לזה מתכנת אחר שדיבר איתי על זה) שהמילה "הרחבה" בהקשר זה לא מדבר על הסט שהוא תוצאה מהגדרת הטייפ אלא על הגדרת הטייפ עצמו,
        ככל שאתה יותר מאריך בהגדרת הטייפ, כך אתה שולל יותר ויותר מועמדים פוטנציאליים.
        לדוגמה: טייפ unknown הוא הטייפ הכי גדול מצד אחד, כי הוא כולל כל הערכים האפשריים, ומצד שני הגדרתו הוא הכי קצר: אין שום הגדרה, כל מה שזז נכנס לתוך הטייפ הזה. ברגע שמרחיבים את ההגדרה, למשל מוסיפים תנאי שיש לו מתודה פלונית, אתה מצר את מעגל החברים בטייפ

        "פונקציית" Omit

        עכשיו נוכל להסביר את הטייפ Omit שבו סיימנו את הפוסט הראשון, וזה אמור להיות מובן בקלות כעת

        הפונקציה מקבלת בפרמטר הראשונה אובייקט, ובשנייה, union של מפתחות שאתה רוצה להשמיט ממנו
        דוגמה לשימוש (מתוך התיעוד הרשמי):

        type Todo = {
          title: string
          description: string
          completed: boolean
          createdAt: number
        }
         
        type TodoPreview = Omit<Todo, "description">;
        

        ℹ שיניתי קצת מהמקור בתיעוד של TS ממילת המפתח interface למילת type רק לשם העקביות כי לא הצגנו interface במאמר זה - יש הבדל קטן בין השניים אבל זה נושא לשיחה אחרת ולא חשוב כעת
        פה אני רוצה להתמקד על האינטואיציות המרכזיות ולא על פרטים קטנים

        והנה קוד המקור של Omit

        type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }
        

        ננתח את זה לחלקים:

        הפרמטרים

        Omit<T, K extends keyof any>
        

        הפונקציה מקבלת שני פרמטרים, הראשון ברור. זה יכול להיות כל טייפ שהוא. השני מוגבל לקבל רק טייפ שמקיים בעצמו התנאי extends keyof any
        מה זה keyof any? זו דרך קצרה להגיד כל טייפ שחוקי להשתמש בו כמפתח של אובייקט כלשהו, שזה כהיום: string | number | symbol. אז הפרמטר השני הוא טייפ שמורכב רק מדברים שמותרים במפתח אובייקט

        ה-body של הפונקציה

        { [P in Exclude<keyof T, K>]: T[P]; }
        

        יש פה שימוש בכמה דברים שהסברנו כבר למעלה, נתחיל מהפנים לחוץ:

        • keyof T = מחזיר union של כל המפתחות של שדות בטייפ T
        • Exclude<keyof T, K>‎ = מוציא מתוך זה את השמות של השדות שאתה רוצה להשמיט בטייפ הסופי
        • מיפוי [P in Exclude<keyof T, K>] מריץ לולאה על כל חבר מבודד מתוך ה-union שהתקבל מהשלב הקודם כאשר הטייפ המבודד מיוצג על ידי המשתנה P
        • { [P in Exclude<keyof T, K>]: T[P]; } = מייצג טייפ שבו עבור כל P בלולאה, יש שדה בשם P וערך מהטייפ T[P] = כלומר הטייפ המקורי של שדה זו בטייפ המקורי

        פסואודו קוד

        והנה פסאודו קוד לקינוח:

        function Omit(t: Type, u: UnionOfKeys) {
          const res = {}
          for (key of Omit(Keyof(t), u).split('|')) {
            res[key] = t[key]
          }
          return res
        }
        

        פשוט ביותר!

        הנושא המקורי (אחרי כל ההקדמות הארוכות)

        נחזור לנושא המקורי שהצגתי בכותרת
        (אם חיכית בקוצר רוח לחלק זה, זה באמת לא כל כך מרעיש עולמות... מצטער...)

        ולשם הקדמה אביא את הטריגר שהביא אותי להבנה זו:
        מישהו שאל: איך אפשר ליצור טייפ שבו מותרים רק שדה x ו-y ולא שום שדה נוסף

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

        type HasFoo = { foo: string }
        

        גם אובייקט שיש לו שדה bar יכול להיות חבר בקובצת HasFoo

        const hasBar = { bar: 1, foo: 'foo' } 
        const test:HasFoo = hasBar // no error
        

        ℹ Object literal may only specify known properties
        אם הייתי כותב בקיצור בקטע הנ"ל:

        const test:HasFoo = { bar: 1, foo: 'foo' }
        

        הייתי מקבל שגיאה זו: Object literal may only specify known properties, and 'bar' does not exist in type 'HasFoo'.(2353)
        אל תתנו לשגיאה זו להטעות אותך! העובדה ש-HasFoo לא מצהיר על שדה bar לא מונע מחברי הסט לשאת שדות נוספות!
        ההתרעה של TS הוא מסיבה אחרת: מכיון שהמשתנה היחיד שקיבל את הערך של { bar: 1, foo: 'foo' } יש לו טייפ שלא כולל את השדה bar, יוצא שבפועל אין דרך להשתמש בשדה bar. כי הגישה דרך משתנה test אסור כי השדה לא כלול בטייפ שלו. רוב הסיכויים הם שקוד כזה הוא שגיאה של המתכנת, לכן TS מתריע.
        לעומת זאת, בדוגמה שהבאתי, ששמרתי את הערך בתוך משתנה אחר לפני ההשמה ל-test, אין על מה להתריע מכיון ששדה bar נשאר נגיש דרך משתנה hasBar

        נחזור לשאלה: איך אפשר לשלול שדות לא מוצהרות מטייפ

        ספוילר: התשובה היא שאי אפשר. וזה מתועד בתשובות ב-stack overflow ו-issues בגיטהאב

        אבל אני כעקשן ניסתי בכל זאת לפתור את זה על ידי הטייפ הבא:

        type T = {
            x: string
            y: number
        }
        
        type Exact<T> = {
            [K in keyof any]?: K extends keyof T ? T[K] : never
        }
        

        מקווה שכעת קל לכם להבין את הקוד

        בתרגום לאנגלית, זה אומר ככה:
        בטייפ Exact<T>‎: קח את הטייפ של כל מפתח חוקי אפשרי (keyof any), עבור כל חבר מתוך מרכיביו בלולאה ותבדוק אם הוא מתוך המפתחות של T, אם כן, הערך של המפתח בתוך התוצאה צריך להיות כמו ב-T (המקור); אחרת הטייפ צריך להיות never - דבר שכמובן לא יכול להיות
        לכאורה יצא לנו טייפ שאוסר מפתחות מיותרות

        התשובה היא מה שכתבתי בכותרת: "טייפ X לא זהה ל-union של כל הערכים האפשריים שהטייפ כולל"

        כלומר, הלולאה של K in keyof any שמפרק את keyof any למרכיביו, ומעביר כל מרכיב בודד בלולאה, לא מפרק את הטייפ string לכל מחרוזת אפשרית. בהקשר זה string הוא אטומי. כמו"כ number.

        היה אפשר לחשוב אחרת, כי סה"כ הטייפ string הוא סה"כ סט שמחובר מכל המחרוזות האפשריים ביקום. אבל לצערנו TS לא מסתכל על זה ככה. אלא מתייחסים ל-string כאטום. ולכן בלולאה K in keyof any יש התייחסות רק ל-string ו-number ו-symbol ולא לערכים הפרטיים שמרכיבים אותם. כמובן, אם ככה, ההמצאה שלי לא תעבוד כי התנאי K extends keyof T אף פעם לא יתקיים

        השלכה נוספת

        עוד דבר שאנשים מבקשים מפעם לפעם (עיין דברי ימי שאלות SO ואישיו-אים של GH) הוא טייפ שמייצג כל מחרוזת אפשרית חוץ ממחרוזות ידועות לשמצה. למשל

        type NotWeekday = Exclude<string, Weekday>
        

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

        type Exclude<T, U> = T extends U ? never : T
        

        הטייפ מתבסס על העובדה שלפני בדיקת התנאי יש פירוק של T למרכיביו ובדיקת התנאי רץ על כל מרכיב בנפרד (Distributive Conditional Types)
        אבל לצערנו, הטייפ string הוא אטומי. הוא לא מתפרק לכל המחרוזות שמרכיבים אותו. לכן התנאי T extends U אף פעם לא מתקיים


        תם ולא נשלם

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

        תגובה 1 תגובה אחרונה
        16
        • dovidD dovid פיצל נושא זה ב

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

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

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