Menu

TypeScript Intersection Types: שילוב טיפוסים עם &

טיפוס intersection מהצורה A & B מתאר ערך שהוא גם A וגם B בו זמנית, ולכן יש לו כל איבר של שניהם. כאן תלמדו איך משלבים טיפוסי אובייקט עם &, למה מאפיינים מתנגשים הופכים ל-never, איך intersection של unions שומר רק את האיברים המשותפים, ומתי עדיף להשתמש ב-extends.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

טיפוס 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 או BA וגם B
קבוצת הערכים המותריםגדולה יותרקטנה יותר
מאפיינים שאפשר להשתמש בהםרק אלה שנמצאים בשניהםכולם, משניהם
string עם numberstring | numbernever
"a" | "b" עם "b" | "c""a" | "b" | "c""b"

ל-intersection של טיפוסי אובייקט יש יותר מאפיינים דווקא מפני שהוא מאפשר פחות ערכים: רק אובייקטים שיש להם הכול.

Intersection מול extends

type C = A & Binterface 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 כשהמאפיינים של האובייקט השני צריכים להחליף את אלה של הראשון.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל