כאשר הצהרות טיפוסים דומות חוזרות ב־TypeScript, מחפשים דרך לחשב את טיפוס התוצאה מתוך טיפוס הקלט. השאלה החשובה היא אם הקשר בין הטיפוסים ברור לקורא, יותר ממספר התווים שנחסכו. במאמר נבחן טיפוסים מותנים ואת infer באמצעות שירות דמיוני לשליחת התראות על משלוחים, ונבחין בינם לבין בדיקה שהנתונים החיצוניים אכן תקינים בפועל.
1. טיפוס מותנה הוא הסתעפות שמקבלת טיפוס כקלט
התיעוד הרשמי על טיפוסים מותנים מסביר את התחביר לבחירת טיפוס תוצאה לפי תנאי extends. הדוגמה הבאה יוצרת טיפוס ליטרלי בוליאני לפי השאלה אם הקלט הוא טיפוס מחרוזת. זו אינה פונקציה שבודקת מחרוזת בזמן ריצה.
type IsText<T> = T extends string ? true : false;
type NameIsText = IsText<'parcel'>; // true
type CountIsText = IsText<3>; // false
נניח שפונקציה שמייצרת התראות על משלוחים מקבלת מזהה מחרוזת, ואילו פונקציה לעיבוד פנימי מקבלת מזהה מספרי. לפני שמשתמשים בטיפוס מותנה, צריך להיות מסוגלים להסביר מדוע מבחינים בין שני המזהים. אם הם מייצגים משמעויות שונות באמת, שמות נפרדים מסייעים; אם זו רק צורת כתיבה שונה, איחוד פורמט הקלט עשוי להיות קריא יותר. אל תסתירו עמימות בתכנון הנתונים באמצעות טכניקות טיפוסים.
בבדיקת טיפוסים, רשמו בזוגות את הקלט ואת התוצאה הצפויה. חלוקה לדוגמאות קטנות של מחרוזת, מספר, union וקלט לא צפוי מפחיתה את העומס שבקריאת הצהרה מורכבת. בדקו גם שהתוצאה הכתובה בהערה תואמת לטיפוס שעורך הקוד מסיק. שם הטיפוס עשוי להישמע טבעי גם כשהתוצאה המחושבת בפועל שונה.
2. התפלגות union משנה את משמעות המערך
כשפרמטר הטיפוס מופיע ישירות בצד השמאלי של התנאי, התנאי מוחל בנפרד על כל אחד מחברי ה־union. כדי למנוע את ההתפלגות ולהתייחס ל־union כולו כיחידה אחת, אפשר להשוות לצורה העוטפת אותו בסוגריים מרובעים.
type SeparateLists<T> = T extends unknown ? T[] : never;
type Together<T> = [T] extends [unknown] ? T[] : never;
type A = SeparateLists<string | number>; // string[] | number[]
type B = Together<string | number>; // (string | number)[]
const a: A = ['a', 'b'];
const b: B = ['a', 1];
הטיפוס הראשון מתאר מערך של מחרוזות בלבד או מערך של מספרים בלבד. הטיפוס השני מאפשר למחרוזות ולמספרים להופיע יחד באותו מערך. התוצאה המתאימה תלויה בשאלה אם כל רשימת המשלוחים חייבת להכיל קודים מסוג אחד בלבד, או אם מותר לכל פריט להכיל קוד מסוג אחר. בשני המקרים רואים סוגריים מרובעים, אך החוזה של שני המערכים אינו זהה.
כשמתעוררת בעיה בעבודה, נסו להציב בכל פעם חבר אחד של טיפוס הקלט, במקום לקרוא את כל טיפוס העזר המורכב בבת אחת. השוו בין התוצאה עבור מחרוזת, עבור מספר ועבור union של שניהם. כשהכוונה להתפלגות ברורה, אפשר להסביר גם מדוע מוסיפים סוגריים מרובעים. בתיעוד בדיקת הטיפוסים כדאי להשאיר דוגמאות לערכים שרוצים לאפשר, ולא רק את התוצאה הסופית.
3. infer נותן שם לטיפוס בתוך מבנה ומחלץ אותו
בתוך טיפוס מותנה משתמשים ב־infer כדי לתת שם לחלק מן המבנה שנמצאה לו התאמה. הדוגמה הבאה היא טיפוס קטן שמסיר שכבה אחת של Promise.
type OneResult<T> = T extends Promise<infer Value> ? Value : T;
type Parcel = OneResult<Promise<{ tracking: string }>>;
type ParcelList = OneResult<Promise<string[]>>;
הקוד הזה אינו מפעיל פעולה אסינכרונית ואינו ממתין לה. הוא רק מבטא את צורת התוצאה בשלב הקומפילציה. שם פונקציה כגון Resolved עלול ליצור רושם שהערך עצמו כבר הגיע, ולכן צריך לבחון גם את ההקשר שבו משתמשים בטיפוס הזה. ההמתנה בזמן ריצה עדיין צריכה להתבצע בקוד אסינכרוני.
אם דרוש טיפוס של תוצאה שממתינים לה באופן רקורסיבי, בדקו קודם את טיפוס העזר הסטנדרטי Awaited, לפני שתמשיכו להרחיב בעצמכם את הדוגמה שמסירה שכבה אחת. הפשטות של טיפוס לימודי שונה מהדרישות מכלי משותף. אם צוות מוסיף טיפוס עזר חדש, כדאי לתעד אילו קלטים מפיקים תוצאה שונה מזו של הטיפוס הסטנדרטי ולמה ההבדל נחוץ.
4. לחבר שמות אירועים באמצעות template literals
טיפוסי template literal מאפשרים לבטא צירופים של ליטרלים של מחרוזות כטיפוס. בשירות משלוחים אפשר להדגים זאת באמצעות שם אירוע שמשלב תחום ופעולה.
type Scope = 'parcel' | 'address';
type Action = 'created' | 'updated';
type EventName = `${Scope}:${Action}`;
const event: EventName = 'parcel:created';
אפשר לזהות שם עם שגיאת כתיב בזמן הקומפילציה, אך השאלה אם כל האירועים שניתן להרכיב תקפים בשירות האמיתי היא שאלה אחרת. למשל, אם נתמך רק אירוע של מחיקת כתובת, מכפלה פשוטה של תחומים ופעולות עלולה לא לבטא את הכלל במדויק. במקרה כזה חשבו תחילה על תכנון שממפה במפורש בין האירועים המותרים לבין ה־payload שלהם.
ככל שמספר האירועים גדל, חשוב גם שהקורא יוכל למצוא בקלות את השמות בפועל. במקום לשרשר כמה שכבות של כללי צירוף אוטומטיים, אפשר לדאוג שהשמות שנשלחים למערכת חיצונית יהיו ניתנים לבדיקה בקובץ אחד. השוו איך חבר צוות חדש יזהה אירוע שגוי לפני הוספת ההפשטה ואחריה. צריך להעריך יחד את התחכום של הטיפוסים ואת נוחות השימוש בהם.
5. JSON חיצוני צריך להיבדק בזמן ריצה
הצמדת טיפוס כמו User או Parcel לתשובת API אינה מבטיחה שהתשובה בפועל בנויה כך. נבחן דרך פשוטה לקבל ערך חיצוני כ־unknown ולבדוק את השדות הדרושים.
function hasTracking(value: unknown): value is { tracking: string } {
return typeof value === 'object' && value !== null
&& 'tracking' in value && typeof value.tracking === 'string';
}
const incoming: unknown = { tracking: 'DEMO-001' };
if (hasTracking(incoming)) console.log(incoming.tracking);
הבדיקה הזאת מוודאת רק שהשדה tracking הוא מחרוזת. היא אינה בודקת שמספר המעקב קיים באמת, או שלמשתמש יש הרשאה לצפות במשלוח הזה. צמצום טיפוס, כללי העבודה של השירות ובדיקת הרשאות הם תחומי אחריות שונים. התיעוד הרשמי על Narrowing מסביר כיצד מידע על טיפוסים מצטמצם באמצעות תנאים.
6. סדר בדיקה קצר לבחירת כלי טיפוסים
תחילה רשמו שני ערכים ממשיים שרוצים לאפשר ושניים שרוצים לדחות. לאחר מכן נסו לבטא את המשמעות בעזרת union רגיל והצהרות אובייקט מפורשות. אם עדיין נשאר קשר שחוזר על עצמו, השימוש בטיפוס מותנה או ב־infer יקבל הצדקה ברורה. אם זמן הקריאה של ההצהרה גדל מאוד, אפשר גם לחלץ רק את החלק המשותף ולהשאיר את היתר כטיפוסים רגילים.
לבסוף הריצו בנפרד בדיקת טיפוסים ובדיקה בזמן ריצה. הבדיקה שהקומפיילר דוחה שם אירוע שגוי והבדיקה שהתוכנית מסבירה את השגיאה כשהשרת מחזיר נתונים שחסר בהם שדה הן בדיקות משלימות. אחרי שמוודאים את תוצאות הדוגמאות הקטנות, צריך לחבר אותן לגבול הקלט האמיתי כדי שהמרת הטיפוסים תסייע לתכנון יישום בטוח.