Exclude<T, U> מסיר איברים מ-union type, Extract<T, U> משאיר רק את האיברים שמתאימים, ו-NonNullable<T> מסיר null ו-undefined. שלושתם מסננים unions; הם לא נוגעים במאפיינים של אובייקטים.
כמו כל utility type, הם קיימים רק בזמן קומפילציה. השורה האחרונה נדחית על ידי הקומפיילר; ל-JavaScript שרץ אין מושג מה זה Visible.
Exclude, Extract ו-NonNullable במבט אחד
| טיפוס | משאיר | דוגמה | תוצאה |
|---|---|---|---|
Exclude<T, U> | איברים של T שאינם ניתנים להשמה ל-U | Exclude<"a" | "b" | "c", "a"> | "b" | "c" |
Extract<T, U> | איברים של T שניתנים להשמה ל-U | Extract<"a" | "b" | "c", "a" | "z"> | "a" |
NonNullable<T> | הכול חוץ מ-null ו-undefined | NonNullable<string | null | undefined> | string |
"ניתן להשמה ל-U" הוא הביטוי החשוב. U לא חייב להיות רשימה של איברים מדויקים; הוא יכול להיות טיפוס רחב יותר שכמה איברים מתאימים לו:
בחירת איברי union לפי מבנה
ב-discriminated union, Extract עם מבנה חלקי שולף את הווריאנטים שיש להם אותו. זו הדרך הרגילה לתת שם לווריאנט אחד של union שלא כתבתם בעצמכם:
המבנה { type: "click" } הוא supertype רק של הווריאנט של click, ולכן זה הווריאנט ש-Extract משאיר. מבנה שמתאים לכמה ווריאנטים (נניח { x: number } אם היה לשניים) משאיר את כולם.
NonNullable
NonNullable<T> הוא הגרסה של הטיפוסים לבדיקה !== null && !== undefined. הוא מופיע כשגוזרים טיפוס ממשהו אופציונלי:
User["manager"] הוא { name: string } | null | undefined (ה-? מוסיף undefined), ו-NonNullable מסיר את שניהם. מאז TypeScript 5.5 הקומפיילר גם מסיק את ה-predicate הזה בעצמו, ולכן ל-raw.filter((s) => s != null) כבר יש את הטיפוס string[]; ה-s is NonNullable<typeof s> המפורש משאיר את הכוונה גלויה ועובד בגרסאות ישנות יותר. ההגדרה שלו היא type NonNullable<T> = T & {}: טיפוס האובייקט הריק {} מקבל כל ערך חוץ מ-null ו-undefined, ולכן חיתוך איתו מסיר בדיוק אותם.
איך הם בנויים
Exclude ו-Extract הם conditional types של שורה אחת בספרייה הסטנדרטית:
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
כש-T הוא union, conditional type מופעל על כל איבר בנפרד (הוא מתפזר), ואיברי never נעלמים מהתוצאה. לכן Exclude<"a" | "b", "a"> הופך ל-("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b"), שהוא never | "b", שהוא "b". העמוד על conditional types מסביר את הפיזור לעומק.
Exclude מול Omit, Extract מול Pick
קל לבלבל בין השמות, כי שני הזוגות "מסירים" או "משאירים" משהו. ההבדל הוא על מה הם פועלים:
| עובד על | מסיר או משאיר | דוגמה | |
|---|---|---|---|
Exclude<T, U> | union | איברי union | Exclude<"a" | "b", "a"> → "b" |
Extract<T, U> | union | איברי union | Extract<"a" | "b", "a"> → "a" |
Omit<T, K> | טיפוס אובייקט | מאפיינים | Omit<User, "password"> |
Pick<T, K> | טיפוס אובייקט | מאפיינים | Pick<User, "name"> |
שימוש ב-Exclude על טיפוס אובייקט הוא טעות נפוצה, והיא נכשלת בשקט:
Account לא ניתן להשמה ל-"password", ולכן Exclude משאיר אותו שלם. Omit עצמו בנוי מ-Exclude שמופעל על המפתחות: Omit<T, K> הוא Pick<T, Exclude<keyof T, K>>. ראו Omit להרחבה.
Exclude מחמיר יותר
הפרמטר השני של Exclude מקבל כל טיפוס, ולכן שגיאת כתיב לא מסירה כלום ולא מעלה שגיאה. הוספת אילוץ גורמת לקומפיילר לבדוק שמה שאתם מסירים באמת נמצא ב-union:
index.ts(7,37): error TS2344: Type '"drafft"' does not satisfy the constraint 'Status'.
תקנו את האיות והקוד יעבור קומפילציה, כש-Strict שווה ל-"published" | "archived". אותו טריק עובד ל-Extract ול-Omit מחמיר (K extends keyof T).
שאלות נפוצות
מה עושה Exclude ב-TypeScript?
Exclude<T, U> מסיר מה-union T כל איבר שניתן להשמה ל-U. Exclude<"a" | "b" | "c", "a"> הוא "b" | "c". כדי להסיר כמה איברים, העבירו union בתור U: Exclude<T, "a" | "b">.
מה ההבדל בין Exclude ל-Extract?
הם הפכים. Exclude<T, U> משאיר את האיברים של T שאינם ניתנים להשמה ל-U; Extract<T, U> משאיר רק את האיברים שכן ניתנים. עבור אותם T ו-U, שתי התוצאות יחד מרכיבות את T.
מה ההבדל בין Exclude ל-Omit?
Exclude עובד על union type ומסיר איברי union. Omit עובד על טיפוס אובייקט ומסיר מאפיינים. Exclude<User, "password"> לא עושה שום דבר שימושי (הוא מחזיר User), ואילו Omit<User, "password"> נותן את טיפוס האובייקט בלי המאפיין הזה. Omit בנוי על Exclude: הוא מסיר את המפתח מ-keyof T.
מה עושה NonNullable ב-TypeScript?
NonNullable<T> מסיר null ו-undefined מטיפוס: NonNullable<string | null | undefined> הוא string. הוא מוגדר כ-T & {}, כי {} מקבל כל ערך חוץ מ-null ו-undefined.
למה Exclude לא מדווח על שגיאה לאיבר שלא קיים?
הפרמטר השני שלו לא מוגבל, ולכן כל טיפוס מתקבל, ושגיאת כתיב כמו Exclude<Status, "drafft"> בשקט לא מסירה כלום. הגדירו גרסה מחמירה יותר, type StrictExclude<T, U extends T> = Exclude<T, U>, כדי שהקומפיילר יבדוק ש-U הוא חלק מ-T.