Union, Narrowing ו-Literal Types
השיעור הזה הוא הלב של TypeScript. Union מתאר ערך שיכול להיות אחד מכמה דברים, ו-Narrowing הוא הדרך שבה TypeScript "מבינה" מתוך תנאי רגיל באיזה מהם מדובר. יחד הם מאפשרים לתאר מצבים אמיתיים של אפליקציה בדיוק.
Union: זה או זה
type Id = number | string;
function printId(id: Id) {
console.log(`המזהה הוא ${id}`);
id.toUpperCase();
// Property 'toUpperCase' does not exist on type 'number'.
}
על Union מותר לעשות רק מה שמותר על כל האפשרויות. כדי להשתמש במשהו ספציפי, צריך לבדוק קודם.
Narrowing: הבדיקה מצמצמת את הטיפוס
function printId(id: number | string) {
if (typeof id === 'string') {
console.log(id.toUpperCase()); // כאן id הוא string
} else {
console.log(id.toFixed(0)); // וכאן number
}
}
אין כאן תחביר מיוחד: אותו if של JavaScript. TypeScript עוקבת אחרי התנאים ויודעת מה הטיפוס בכל ענף. דרכים נפוצות לצמצם:
| בדיקה | דוגמה |
|---|---|
typeof | typeof value === 'string' |
| אמת/שקר | if (user) מוציא את null ו-undefined |
Array.isArray | Array.isArray(value) |
in | 'email' in contact |
instanceof | error instanceof Error |
| השוואה לערך | status === 'done' |
דוגמה יומיומית: catch
try {
await saveOrder(order);
} catch (error) {
// error הוא unknown: כל דבר יכול להיזרק ב-JavaScript
const message = error instanceof Error ? error.message : String(error);
showToast(`השמירה נכשלה: ${message}`);
}
Discriminated Union: מצבים שלא מתערבבים
הכלי החזק ביותר בשיעור. כל מצב מקבל שדה משותף עם ערך קבוע (כאן status), ולכל מצב יש רק את השדות שהגיוניים בו:
type RequestState =
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'success'; data: string[] };
function render(state: RequestState): string {
switch (state.status) {
case 'loading':
return 'טוען...';
case 'error':
return `שגיאה: ${state.message}`; // message קיים רק כאן
case 'success':
return `נמצאו ${state.data.length} תוצאות`; // data קיים רק כאן
}
}
השוו לחלופה הנפוצה: { loading: boolean; error?: string; data?: string[] }. שם אפשר בטעות להיות גם בטעינה וגם עם שגיאה, ו-data תמיד "אולי undefined". עם Discriminated Union מצבים בלתי אפשריים פשוט לא ניתנים לכתיבה.
לוודא שטיפלתם בכל המקרים
function assertNever(value: never): never {
throw new Error(`מצב לא צפוי: ${JSON.stringify(value)}`);
}
function icon(state: RequestState): string {
switch (state.status) {
case 'loading': return '⏳';
case 'error': return '❌';
case 'success': return '✅';
default: return assertNever(state);
}
}
מחר מישהו מוסיף מצב 'idle' לטיפוס? כל switch שלא טיפל בו יסומן בשגיאה, כי state כבר לא never ב-default.
Type Guard: בדיקה משלכם
type Cat = { meow: () => void };
type Dog = { bark: () => void };
// value is Cat: "אם הפונקציה מחזירה true, זה Cat"
function isCat(animal: Cat | Dog): animal is Cat {
return 'meow' in animal;
}
function speak(animal: Cat | Dog) {
if (isCat(animal)) animal.meow();
else animal.bark();
}
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה זה Narrowing?
הצגת התשובה
תשובה ד. אחרי if (typeof x === 'string'), TypeScript יודעת ש-x הוא string.
-
מה היתרון של Discriminated Union לתיאור מצב טעינה?
הצגת התשובה
תשובה א. כל מצב מחזיק רק את השדות שהגיוניים בו.
-
מה הטיפוס של error בתוך
catchעם strict?הצגת התשובה
תשובה ד. אפשר לזרוק כל דבר ב-JavaScript, ולכן בודקים error instanceof Error.