טיפוס intersection, שנכתב A & B, מתאר ערך שהוא A וגם B באותו זמן. בטיפוסי אובייקט זה אומר שלערך יש כל מאפיין של שניהם. כך משלבים טיפוסים קיימים בלי לכתוב שוב את המאפיינים.
באובייקט השני חסר team, ולכן הוא שגיאת הקומפילציה TS2322, והשורה הבאה בהודעה אומרת Property 'team' is missing ... but required in type 'Employee'. אפשר להעביר ערך Staff בכל מקום שבו מצופה Person או Employee.
שילוב טיפוסי אובייקט
& עובד עם כל שילוב של type aliases, interfaces וטיפוסי אובייקט שנכתבו במקום, וגם עם פרמטרי טיפוס גנריים. במקרה האחרון קשה להחליף אותו: פונקציה שמוסיפה מאפיינים לכל אובייקט שהיא מקבלת יכולה לומר את זה בדיוק.
מי שקורא לפונקציה שומר על הטיפוס המדויק של מה שהעביר (title, words), ובנוסף שני המאפיינים שנוספו. interface ... extends לא יכול לבטא "מה שלא יהיה T, ועוד אלה", כי interface לא יכול להרחיב פרמטר טיפוס.
מאפיינים מתנגשים הופכים ל-never
כששני הצדדים מצהירים על אותו מאפיין, הטיפוס של המאפיין הוא ה-intersection של השניים. אם לטיפוסים האלה אין אף ערך משותף, המאפיין הופך ל-never, והקומפיילר לא אומר כלום עד שמנסים ליצור ערך:
index.ts(7,22): error TS2322: Type 'string' is not assignable to type 'never'.
השגיאה מצביעה על האובייקט, לא על הטיפוס שגרם לה, ובגלל זה קשה לאתר באגים כאלה. מעבר עם העכבר מעל r.id בעורך מראה את הטיפוס שלו: never. כשהמאפיין המתנגש הוא תג ליטרל, כמו ב-type Shape = { kind: "circle" } & { kind: "square" }, TypeScript הולך רחוק יותר ומצמצם את כל ה-intersection ל-never. קריאה של מאפיין בערך כזה מסבירה אז למה: Property 'kind' does not exist on type 'never'. The intersection 'Shape' was reduced to 'never' because property 'kind' has conflicting types in some constituents.
interface ... extends תופס את אותה התנגשות כבר בהצהרה, עם השגיאה TS2430. זה ההבדל המעשי העיקרי בין השניים; העמוד interface מול type משווה ביניהם זה לצד זה.
חפיפות תואמות מצמצמות את המאפיין
אם שני טיפוסי המאפיין כן חופפים, התוצאה היא החפיפה. זה שימושי, לא שגיאה:
החצי השני מראה מה & עושה ל-unions: הוא שומר את האיברים ששני הצדדים חולקים. חשיבה על טיפוסים כעל קבוצות של ערכים הופכת את זה לצפוי. A | B הוא האיחוד של שתי הקבוצות, A & B הוא החפיפה שלהן, וחפיפה ריקה היא never.
Intersection מול union
השמות באים מתורת הקבוצות, והם מרגישים הפוכים כשמחילים אותם על מאפיינים של אובייקטים:
A | B (union) | A & B (intersection) | |
|---|---|---|
| ערך הוא | A או B | A וגם B |
| קבוצת הערכים המותרים | גדולה יותר | קטנה יותר |
| מאפיינים שאפשר להשתמש בהם | רק אלה שנמצאים בשניהם | כולם, משניהם |
string עם number | string | number | never |
"a" | "b" עם "b" | "c" | "a" | "b" | "c" | "b" |
ל-intersection של טיפוסי אובייקט יש יותר מאפיינים דווקא מפני שהוא מאפשר פחות ערכים: רק אובייקטים שיש להם הכול.
Intersection מול extends
type C = A & B | interface C extends A, B | |
|---|---|---|
| עובד עם | כל טיפוס, כולל unions ופרמטרי טיפוס | טיפוסי אובייקט עם איברים שידועים סטטית |
| מאפיין מתנגש | הופך בשקט ל-never | השגיאה TS2430 או TS2320 בהצהרה |
| תוצאה | intersection, שנבדק רכיב אחרי רכיב | טיפוס שטוח אחד עם שם, שהיחסים שלו נשמרים במטמון |
| הרכבות גדולות | יכולות להאט את בדיקת הטיפוסים | מומלץ בעמוד הביצועים בוויקי של TypeScript |
לשילוב של כמה type aliases של אובייקטים, & הוא הדרך המקובלת והוא בסדר גמור. לטיפוס שנבנה מהרבה חלקים, או לטיפוס של API ציבורי, extends נותן שגיאות מוקדמות יותר ובדיקות טיפוסים זולות יותר.
Intersections עם טיפוסים פרימיטיביים: branding
intersection של טיפוס פרימיטיבי עם טיפוס אובייקט לא מייצר never: string & { readonly __brand: "UserId" } היא מחרוזת שנושאת סימון נוסף שקיים רק בזמן הקומפילציה. לאף מחרוזת אמיתית אין את המאפיין הזה, וזו בדיוק הנקודה: רק קוד שמצהיר על ה-brand בכוונה יכול ליצור כזו, ולכן כבר אי אפשר להעביר string רגיל או OrderId במקום שבו מצופה UserId. לטכניקה הזו יש עמוד משלה, branded types.
שאלות נפוצות
מה זה טיפוס intersection ב-TypeScript?
טיפוס שנכתב A & B שהערכים שלו חייבים לעמוד גם ב-A וגם ב-B באותו זמן. בטיפוסי אובייקט זה אומר שלערך יש כל מאפיין של A וכל מאפיין של B. זו הדרך הרגילה לשלב שני type aliases לאחד.
מה ההבדל בין union ל-intersection?
union מהצורה A | B פירושו "אחד מהשניים": הערך יכול להיות A או B, ואפשר להשתמש רק במה שמשותף להם עד שמצמצמים. intersection מהצורה A & B פירושו "שניהם": לערך יש הכול משניהם. בטיפוסי אובייקט, ה-union מקבל יותר ערכים ולטיפוס ה-intersection יש יותר מאפיינים.
למה טיפוס ה-intersection שלי הוא never?
כי אף ערך לא יכול לעמוד בשני הצדדים. string & number הוא never, ו-{ id: string } & { id: number } הופך את id ל-string & number, כך שהמאפיין הוא never ואי אפשר ליצור אף אובייקט. אם לשני טיפוסי אובייקט יש אותו תג ליטרל עם ערכים שונים (kind: "circle" ו-kind: "square"), כל ה-intersection מצטמצם ל-never.
כדאי להשתמש ב-intersection או ב-extends?
שניהם משלבים טיפוסי אובייקט. interface X extends A, B מדווח על מאפיינים מתנגשים בהצהרה, וצוות TypeScript ממליץ עליו להרכבה של טיפוסי אובייקט גדולים. & עובד עם כל טיפוס, כולל unions ופרמטרים גנריים, ש-extends לא יכול לשלב. השתמשו ב-& ל-type aliases ולפונקציות עזר גנריות, וב-extends כשבונים interfaces.
איך ממזגים שני טיפוסי אובייקט ב-TypeScript?
כותבים type Merged = A & B. בשביל הערך בזמן ריצה, עושים spread לשני האובייקטים: const merged: A & B = { ...a, ...b }. אם ל-A ול-B יש מאפיין משותף עם טיפוסים שונים, הטיפוס של המאפיין הזה הופך ל-never; השתמשו ב-Omit<A, keyof B> & B כשהמאפיינים של האובייקט השני צריכים להחליף את אלה של הראשון.