באתר הרשמה לסדנאות יש שדה של מספר משתתפים. המבקר מקליד שניים והכפתור שולח את הבקשה. הכול נראה פשוט עד שמגלים שבמקום אחד המספר נשמר כמספר ובמקום אחר הוא נשאר טקסט. בהמשך התהליך פעולה שאמורה לחבר מספרים מחברת מחרוזות. זו לא בעיית עיצוב וגם לא בהכרח תקלה בשרת. שני חלקים באתר פשוט הניחו דברים שונים על אותו ערך.
TypeScript עוזרת לזהות חוסר התאמה כזה בזמן הפיתוח. היא מוסיפה ל־JavaScript מערכת טיפוסים שמתארת אילו ערכים הקוד מצפה לקבל ולהחזיר. הערך המעשי שלה אינו בכך שכל משתנה מקבל תווית. הוא מופיע כשמשנים שדה או מעבירים נתונים בין חלקים במערכת ורוצים לדעת מי עדיין משתמש בהנחות הישנות.
כדי להבין איפה היא מועילה ואיפה היא נעצרת נעקוב אחרי טופס הרשמה לסדנת צילום. זו דוגמה למערכת עם ממשק בדפדפן ושרת שמקבל בקשות. אותה דרך חשיבה מתאימה לאתרי תוכן עם רכיבים מתקדמים ולמערכות גדולות יותר. אין צורך להתחיל ממערכת טיפוסים מסובכת כדי להפיק ממנה תועלת.
הטיפוס מתאר את הערך שהקוד מצפה לקבל
כאשר פונקציה מקבלת מספר משתתפים מסוג number, בודק הטיפוסים יכול להתריע על קריאה שמעבירה לה טקסט. הוא יכול לזהות גם ניסיון לגשת לשדה שלא הוגדר באובייקט או שימוש בתוצאת פונקציה כאילו החזירה משהו אחר. אלה בדיקות של הקוד וההצהרות שנמצאות לפניו בזמן הפיתוח.
חשוב להבין מה הן לא אומרות. הטיפוס number אינו מבטיח שמספר המשתתפים חיובי או שלם. גם ערך שלילי הוא מספר מבחינת JavaScript. אם אפשר לרשום עד שישה משתתפים בבקשה אחת, זו כבר דרישה שצריך לבדוק. שם של טיפוס יכול לתעד את הכוונה אבל אינו הופך כל ערך ששויך אליו לתקין.
גם לא צריך לכתוב טיפוס מפורש ליד כל משתנה. TypeScript מסוגלת להסיק טיפוסים מתוך ערכי התחלה ומתוך השימוש בפונקציות. כדאי להשקיע בהגדרות שבהן יש החלטה חשובה: מה נכנס לפונקציה משותפת ומה יוצא ממנה או מהו המבנה של תשובה שהממשק מקבל מהשרת. תוספת סימונים שאינה מוסיפה מידע רק מאריכה את הקוד.
טופס מתחיל בטקסט גם כשהוא מבקש מספר
שדה בדפדפן שנראה כמו שדה מספר אינו מבטיח שהערך שקוראים ממנו כבר מתאים למודל של המערכת. אם קוראים את value של שדה קלט מקבלים טקסט. גם בקריאת FormData צריך לבדוק מה התקבל לפני שמשתמשים בו. זה המקום להפריד בין הנתון הגולמי מהטופס לבין הערך שהיישום מוכן לעבוד איתו.
בדוגמה שלנו אפשר להגדיר פונקציה קטנה שמקבלת ערך לא ידוע ומחזירה מספר תקין או null. הדרישה המקומית היא בין משתתף אחד לשישה. הקוד הבא מטפל בערך של שדה הטופס בלבד. הוא אינו בודק את כל פרטי ההרשמה ואינו מאשר שיש מקום בסדנה.
function readParticipants(value: unknown): number | null {
if (typeof value !== "string" || value.trim() === "") {
return null;
}
const count = Number(value);
return Number.isInteger(count) && count >= 1 && count <= 6
? count
: null;
}
כאן יש שני דברים נפרדים. הבדיקות בתוך הפונקציה רצות בפועל ובוחנות את הערך. הטיפוס של התוצאה מזכיר לקוד שקורא לה שייתכן כישלון. אם התקבל null, אפשר להציג הסבר ליד השדה. אם התקבל מספר, אפשר להעביר אותו לשלב הבא בלי להתייחס אליו שוב כטקסט.
הבדיקה של שדה ריק אינה מיותרת. המרה באמצעות Number לבדה יכולה להחזיר אפס עבור מחרוזת ריקה. ההחלטה אם שדה ריק הוא ערך חסר או בחירה באפס שייכת להתנהגות הטופס. במקרה הזה אלה מצבים שונים ולכן מפרידים ביניהם לפני ההמרה. טיפוסים מועילים במיוחד כאשר הם גורמים לנו להגדיר הבדלים כאלה במפורש.
JSON מהשרת לא מגיע עם אישור של TypeScript
אחרי השליחה הממשק עשוי לקבל תשובת JSON שמכילה מזהה הרשמה ומצב אישור. אפשר לכתוב טיפוס שמייצג את התשובה הצפויה. אבל ההגדרה הזאת אינה קוראת את המידע שהגיע מהרשת. השרת עשוי להחזיר תשובה אחרת בגלל שינוי גרסה או תקלה. יכול להיות שחסר שדה או שהוחזר דף שגיאה במקום JSON.
כתיבה של as ליד תוצאת הפענוח אינה אימות. זו הצהרה של המתכנת כלפי בודק הטיפוסים. היא אינה מוסיפה בדיקה בזמן ריצה ואינה מתקנת שדות חסרים. גם טיפוס שמופיע ליד משתנה לא מוודא מעצמו שהנתונים שהתקבלו מבחוץ תואמים לו. הצהרות הטיפוסים אינן מנגנון שבוחן בקשות בזמן שהאתר פועל.
דרך שימושית לחשוב על הגבול הזה היא לקבל מידע חיצוני כ־unknown. כך אי אפשר להשתמש בשדות שלו בלי לבדוק קודם מה הוא מכיל. אפשר לבצע בדיקות קטנות בקוד או להשתמש בספריית אימות עם סכימה. בשני המקרים צריך לוודא שהבדיקה עצמה מופעלת במקום שבו המידע נכנס למערכת.
הבדיקה יכולה לכלול את מבנה האובייקט ואת סוגי השדות. דרישות נוספות תלויות בתחום: למשל שמזהה הרשמה אינו ריק ושמצב האישור הוא אחד המצבים המוכרים. בדיקה כזאת אינה מוכיחה שההרשמה שייכת למועד הנכון. יש הבדל בין מידע בעל מבנה תקין לבין מידע שהמשמעות שלו נכונה.
כדאי לשים לב גם לתאריכים. ערך תאריך שמגיע ב־JSON הוא בדרך כלל טקסט בהתאם לחוזה שבין המערכות. הגדרה שלו כ־Date בטיפוס מקומי אינה יוצרת אובייקט תאריך. אם הקוד צריך אובייקט כזה, נדרשת המרה ובדיקה מתאימה. זהו עוד מקום שבו דמיון בין שמות יכול להסתיר הבדל בין ייצוגים.

שדה משותף אינו אומר שכל חלקי המערכת צריכים אותו מבנה
לטופס ההרשמה יש מידע שהמבקר עורך. לבקשה שנשלחת לשרת יש מבנה מוסכם. לתשובה שמגיעה מהשרת יש שדות אחרים. אפשר לקרוא לכולם “הרשמה” אבל הם אינם אותו דבר. במצב עריכה מספר המשתתפים עשוי להיות עדיין ריק. בבקשה תקינה הוא צריך להיות מספר. בתשובת האישור נוסף מזהה שלא היה קיים קודם.
הפרדה בין הטיפוסים האלה מונעת צורך להפוך כל שדה לאפשרי בכל מקום. במקום אובייקט גדול שבו כמעט הכול מסומן כאופציונלי, אפשר לתאר כל שלב לפי מה שבאמת ידוע בו. כך גם קל יותר להבין מה פונקציה מסוימת אמורה לעשות. פונקציה שמציגה אישור לא צריכה לקבל טיוטת טופס לא גמורה.
כאשר הדפדפן והשרת כתובים ב־TypeScript אפשר לשתף ביניהם הגדרות שמתארות את התקשורת. השיתוף מצמצם כפילות ומסייע לגלות שינוי בשמות השדות. הוא אינו מחליף אימות בזמן ריצה. שני הצדדים יכולים להשתמש באותו קובץ טיפוסים ובכל זאת להיפרס בזמנים שונים או לקבל מידע ממקור נוסף.
גם שרת שנכתב בשפה אחרת יכול לעבוד היטב עם ממשק TypeScript. צריך חוזה נתונים ברור ולא זהות בין שפות הפיתוח. אפשר לייצר טיפוסים מתוך תיאור של ה־API כאשר הפרויקט משתמש בכלים מתאימים. ההבדל בין שפה לבין סביבת ריצה ומסגרת פיתוח מוסבר במדריך על שפות וטכנולוגיות לבניית אתרים.
אפשר לתאר מצב מסך בלי אוסף דגלים שסותרים זה את זה
ממשק הרשמה צריך לדעת אם המשתמש עדיין עורך ואם בקשה נשלחת או כבר אושרה. דרך נפוצה היא לשמור כמה ערכים נפרדים כמו isLoading ו־isSuccess ולצדם הודעת שגיאה. כך קל להגיע לצירוף לא ברור שבו המסך גם טוען וגם מציג הצלחה ושגיאה קודמת.
אפשר לתאר במקום זאת כמה מצבים חלופיים. לכל מצב יש שם ברור ורק המידע ששייך אליו. זה נקרא איחוד מובחן. השדה status מאפשר ל־TypeScript להבין באיזה חלק של האיחוד אנחנו נמצאים ולהתאים את השדות הזמינים.
type RegistrationState =
| { status: "editing" }
| { status: "sending" }
| { status: "confirmed"; reference: string }
| { status: "failed"; message: string };
function messageFor(state: RegistrationState): string {
if (state.status === "confirmed") {
return "מספר ההרשמה: " + state.reference;
}
if (state.status === "failed") return state.message;
return state.status === "sending"
? "ההרשמה נשלחת"
: "מלאו את הפרטים";
}
במצב confirmed חייב להופיע reference. במצב failed יש message. הקוד אינו צריך לנחש אם מזהה ההרשמה כבר קיים בזמן שהבקשה נשלחת. אם ננסה ליצור אובייקט אישור בלי המזהה הנדרש, בודק הטיפוסים יוכל להצביע על הבעיה לפני הרצת המסך.
ההגדרה עדיין אינה קובעת את כל סדר המעברים. אפשר לכתוב קוד שמעביר בטעות את המסך למצב confirmed לפני שהשרת אישר. זו תהיה טעות בהתנהגות גם אם צורף מזהה מסוג string. הטיפוס מצמצם צירופים לא רצויים של מידע אבל אינו יודע מעצמו מתי מותר להכריז על הצלחה.
ערך חסר צריך להוביל להחלטה
כאשר strictNullChecks מופעלת, הערכים null ו־undefined מובחנים מערכים רגילים. אם חיפוש ברשימה עשוי לא למצוא סדנה, אי אפשר להשתמש בתוצאה כאילו נמצאה בוודאות. צריך לבחור מה לעשות במקרה החסר: להציג הודעה מתאימה או לאפשר בחירה אחרת או להפסיק את הפעולה הנוכחית.
כדאי להבחין גם בין שדה שיכול להיעדר לבין שדה שמופיע עם ערך null. בבקשת עדכון, היעדר של שדה יכול לומר שלא ביקשו לשנות אותו. ערך null יכול לייצג בקשה להסיר מידע קודם אם כך הוגדר החוזה. אלה לא חייבות להיות המשמעויות בכל מערכת אבל צריך לבחור אותן במפורש. TypeScript עוזרת לתאר את ההבדל אחרי שהוחלט מה הוא אומר.
זו אינה רק דרך להשתיק שגיאה בעורך. היא חושפת מסך שלעיתים לא עוצב כלל. מה רואה מי שפותח קישור לסדנה שכבר הוסרה? האם טקסט הכותרת נשאר ריק או שמופיע הסבר? החלטה מפורשת לגבי ערך חסר יכולה לשפר גם את חוויית השימוש.
סימן קריאה אחרי ערך יכול לומר ל־TypeScript שהערך אינו חסר. הוא לא מוסיף בדיקה שמבטיחה זאת בזמן ריצה. לפני שמשתמשים בו כדאי להבין על איזו עובדה נשענת ההנחה. אם המידע תלוי בבקשת רשת שעדיין לא הסתיימה, העובדה הזאת עשויה להשתנות בין הרצות.
שינוי בשם של שדה מגלה איפה נמצאות ההנחות הישנות
נניח שהחלטנו להחליף את השם reference בשם מפורש יותר כמו registrationId. במערכת עם טיפוסים משותפים אפשר לשנות את ההגדרה ולגלות אילו רכיבים ופונקציות עדיין מצפים לשם הקודם. אולי מסך האישור עוד משתמש בו ואולי גם רכיב שמציג הרשמות קודמות. כך השינוי הופך מרשימת מקומות שצריך לזכור לשיחה עם כלי הפיתוח על שימושים שאינם תואמים עוד.
ההגנה הזאת תלויה במה שהקוד מתאר. אם חלק מהתשובות הוגדרו כ־any, ייתכן שהשימוש בשם הישן לא יזוהה שם. גם מפתח שנבנה מטקסט בזמן ריצה אינו תמיד קשר שבודק הטיפוסים מסוגל להסיק. לכן כדאי לבחון אזורים שהבדיקה לא מכסה ולא לפרש היעדר שגיאות כהוכחה שאין עוד שימושים.
יש גם מידע שכבר נמצא מחוץ לקוד. שינוי שם בטיפוס אינו משנה אוטומטית תשובות של שרת שכבר פורסם או נתונים שמורים. אם הממשק החדש עולה לפני השרת החדש, הוא עלול לצפות לשדה שעדיין אינו נשלח. לפעמים צריך לתמוך בשני המבנים לתקופת מעבר או לתאם את הפריסה. זו החלטה של תחזוקת המערכת ולא תכונה שאפשר לקבל רק מהחלפת שם בעורך.
הרצת הקובץ ובדיקת הטיפוסים הן שתי פעולות שונות
גם בסביבת Node.js שתומכת בהרצת קובצי TypeScript יש להבחין בין הסרת טיפוסים לבין בדיקתם. המנגנון המובנה של הסרת טיפוסים מאפשר להריץ תחביר נתמך אבל אינו מבצע בדיקת טיפוסים. עצם העובדה שקובץ עם סיומת ts התחיל לרוץ אינה אומרת שכל ההצהרות שבו נבדקו.
כדאי להגדיר בדיקת טיפוסים מפורשת כחלק מתהליך העבודה. בפרויקט מתאים אפשר להריץ tsc עם האפשרות noEmit כדי לבדוק בלי לייצר קובצי פלט. חשוב גם שהתהליך האוטומטי יתייחס לכישלון הבדיקה ולא ימשיך כאילו לא קרה דבר. תהליך שמתרגם קוד במהירות אינו בהכרח אותו תהליך שבודק אותו.
לצד הבדיקה הסטטית עדיין נדרשות בדיקות של התנהגות. בדוגמה שלנו כדאי לבדוק קלט חסר ואת גבולות מספר המשתתפים ולוודא שמצב אישור מופיע רק אחרי תשובה מתאימה. את חלוקת האחריות בין השרת לדפדפן צריך להגדיר בנפרד. TypeScript אינה מחליפה את ההבחנה בין קוד שרץ בשרת לקוד שרץ בדפדפן.
שאלות מעשיות לפני שמוסיפים TypeScript לפרויקט
האם חייבים להמיר מיד את כל קובצי JavaScript?
לא. אפשר להתחיל מקטעים חדשים או מגבולות שבהם עוברים נתונים חשובים. בפרויקט JavaScript אפשר להוסיף מידע באמצעות JSDoc ולהפעיל בדיקת טיפוסים לקבצים מסוימים עם ts-check. מעבר הדרגתי מאפשר ללמוד מה ההגדרות חושפות בלי לערבב באותו שינוי גם החלפת שפה וגם שינוי התנהגות.
האם any הוא פתרון סביר כשספרייה מחזירה מידע לא ברור?
לפעמים נדרש גשר זמני לקוד שלא מתואר היטב. אבל any מאפשר לפעולות להמשיך בלי בדיקות משמעותיות על הערך. רצוי לתחום אותו ולברר מה באמת מתקבל. כאשר המידע אינו מוכר, unknown יחד עם בדיקה מתאימה שומר על ההבחנה בין מה שיודעים לבין מה שרק מניחים.
האם קוד שנוצר עם AI נעשה אמין אם הוא עובר בדיקת טיפוסים?
הבדיקה יכולה למצוא חוסר התאמה בין טיפוסים אבל אינה בוחנת את כל משמעות התהליך. קוד יכול להציג הודעת הצלחה מוקדם מדי או להתעלם מבחירה של המשתמש ועדיין לעבור אותה. כדאי להשתמש ב־TypeScript כדי לנסח גבולות ברורים ואז לבדוק שהפעולות עצמן תואמות למה שהאתר אמור לעשות.



