Union type מפרט אפשרויות עם |: ערך מטיפוס string | number הוא מחרוזת או מספר. בעזרת unions TypeScript מתאר ערכים שיכולים באופן לגיטימי להופיע ביותר מצורה אחת, והקומפיילר מחייב לבדוק איזו צורה יש לכם לפני שמשתמשים במשהו שייחודי לה.
בתוך כל ענף של בדיקת ה-typeof, ל-id יש טיפוס יחיד. לשלב הזה קוראים narrowing, והוא מה שהופך unions לשימושיים.
מותר רק מה שמשותף לכל האיברים
לפני הצמצום, אפשר להשתמש רק במה שכל איבר של ה-union תומך בו. toString() קיים גם על מחרוזות וגם על מספרים, ולכן זה תקין; toUpperCase() קיים רק על מחרוזות:
index.ts(3,16): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.
השורה השנייה מציינת את האיבר שחסר לו המאפיין. אותו כלל חל גם בכיוון ההפוך: אי אפשר להעביר ערך מטיפוס string | number לפרמטר מטיפוס string (TS2345), כי הוא עשוי להיות מספר. Union מקבל יותר ערכים, ובתמורה מאפשר לעשות איתם פחות עד שבודקים.
צמצום של union
צמצום משתמש בבדיקות JavaScript רגילות. TypeScript עוקב אחרי זרימת הבקרה ומסיר איברים ככל שהם נפסלים, כך שאחרי הבדיקה האחרונה נשאר רק איבר אחד:
| בדיקה | מצמצמת | מתאימה ל |
|---|---|---|
typeof x === "string" | לטיפוס הפרימיטיבי | string, number, boolean, bigint, symbol, undefined, function |
x === null, x === "a" | לערך שמושווה | null, undefined, איברים ליטרליים |
Array.isArray(x) | לאיבר שהוא מערך | מערכים |
x instanceof Date | למחלקה | מופעים של מחלקות |
"meow" in x | לאיברים שיש להם את המאפיין | טיפוסי אובייקטים |
x.kind === "circle" | לאיבר עם התגית הזו | discriminated unions |
isCat(x) (מחזירה x is Cat) | למה שהפונקציה אומרת | כל דבר, לוגיקה מותאמת |
הרשימה המלאה של צורות הצמצום נמצאת בעמוד type narrowing.
Unions של טיפוסים ליטרליים
Union של ערכים ליטרליים הוא קבוצה סגורה של ערכים מותרים. זה ה-union הנפוץ ביותר בקוד אמיתי:
type Status = "idle" | "loading" | "success" | "error";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Toggle = "on" | "off" | boolean; // boolean is itself true | false
let current: Status = "idle";
current = "loading"; // fine
current = "finished"; // error TS2322: Type '"finished"' is not assignable to type 'Status'.
השוואה לליטרל מצמצמת: אחרי if (current === "error"), ענף ה-else יודע ש-current הוא אחד משלושת האחרים. Unions של ליטרלים מחליפים enums בהרבה בסיסי קוד; ראו literal types על as const ועל איך גוזרים union כזה ממערך.
Unions של טיפוסי אובייקטים
כשהאיברים הם טיפוסי אובייקטים, מאפיינים שמשותפים לכולם זמינים ישירות. לשאר, בדקו שהמאפיין קיים עם in:
עבור unions של כמה מבני אובייקטים, הדפוס הנקי יותר הוא מאפיין ליטרלי משותף כמו kind: "cat" / kind: "fish". בדיקה של המאפיין הזה בלבד מצמצמת את האובייקט כולו, ואפשר לבדוק ש-switch עליו מכסה את כל המקרים. הדפוס הזה נקרא discriminated union.
מערכים ו-unions
המיקום של הסוגריים משנה את המשמעות לגמרי:
| טיפוס | פירוש | ערך לדוגמה |
|---|---|---|
(string | number)[] | מערך שכל אחד מהאיברים שלו הוא מחרוזת או מספר | [1, "two", 3] |
string[] | number[] | מערך של מחרוזות בלבד, או מערך של מספרים בלבד | ["a", "b"] |
string | number[] | מחרוזת, או מערך של מספרים (| נקשר חלש יותר מ-[]) | "text" |
במעבר על (string | number)[], כל איבר הוא ה-union וצריך צמצום, כמו ב-callback של reduce שלמעלה. מתודות כמו map ו-filter עובדות גם על string[] | number[], וה-callback מקבל string | number.
Unions עם null ו-undefined
ה-union הנפוץ מכולם הוא "ערך או כלום": string | null, User | undefined. זה מה ש-Array.prototype.find ו-Map.prototype.get מחזירים, ומאפיין אופציונלי name?: string נקרא כ-string | undefined. לטיפול בהם עם ?., ?? ובדיקות null יש עמוד משלו: null ו-undefined.
כדי להסיר איברים מ-union קיים ברמת הטיפוסים, השתמשו בטיפוסי העזר המובנים: Exclude<"a" | "b" | "c", "a"> הוא "b" | "c", ו-NonNullable<string | null> הוא string.
שאלות נפוצות
מה זה union type ב-TypeScript?
טיפוס שמורכב מכמה אפשרויות שמחוברות עם |. ערך מטיפוס string | number יכול להיות מחרוזת או מספר. הקומפיילר מאפשר להשתמש רק במה שמשותף לכל האיברים, עד שמצמצמים את הערך לאיבר אחד עם בדיקה כמו typeof value === "string".
למה TypeScript אומר שמאפיין לא קיים על union type?
כי לפחות לאיבר אחד של ה-union אין אותו. השגיאה TS2339, למשל Property 'toUpperCase' does not exist on type 'string | number', אומרת שהערך עשוי להיות מספר, ולמספר אין toUpperCase. צמצמו קודם (typeof, in, Array.isArray, instanceof, או בדיקה של מאפיין מבחין), ואז השתמשו במאפיין שייחודי לאיבר.
איך מצהירים על מערך שמחזיק יותר מטיפוס אחד?
שימו את ה-union בסוגריים: (string | number)[] או Array<string | number>, שבו כל איבר יכול להיות כל אחד מהטיפוסים. string[] | number[] זה משהו אחר: המערך כולו הוא רק מחרוזות או רק מספרים. בלי סוגריים, string | number[] פירושו מחרוזת או מערך של מספרים.
מה ההבדל בין union ל-intersection type?
Union A | B הוא ערך שהוא אחד מהטיפוסים, ולכן אפשר להשתמש רק במה שמשותף להם. Intersection A & B הוא ערך שהוא שניהם בבת אחת, ולכן יש לו את כל האיברים של שניהם. בטיפוסי אובייקטים, A | B מקבל יותר ערכים ו-A & B דורש יותר מאפיינים.
איך בודקים מאיזה טיפוס ערך של union?
השתמשו בבדיקה בזמן ריצה ש-TypeScript מבין: typeof x === "string" לטיפוסים פרימיטיביים, Array.isArray(x) למערכים, x instanceof Date למחלקות, "prop" in x למבני אובייקטים, או x.kind === "circle" כשהאיברים חולקים תגית ליטרלית. ללוגיקה מותאמת, כתבו פונקציית type guard שמחזירה x is T.