Menu

TypeScript Best Practices: 8 כללים עם לפני ואחרי

שמונה הרגלים ב-TypeScript שמונעים באגים אמיתיים: להשאיר את strict פעיל, להשתמש ב-unknown במקום any, לתת להסקת הטיפוסים לעבוד, להעדיף unions על פני enums, לבדוק קונפיגורציה עם satisfies, לתאר מצב עם discriminated unions, להימנע מ-! ומ-as, ולהפוך מידע ל-readonly. לכל אחד יש דוגמת לפני ואחרי שאפשר להריץ.

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

הכללים הבאים הם אלה שמונעים הכי הרבה באגים בקוד TypeScript אמיתי. כל אחד מציג קודם את הגרסה הנפוצה ואחר כך את הגרסה הטובה יותר, בקוד שאפשר להריץ. הכלל הראשון הכי חשוב: הפסיקו להשתמש ב-any.

השתמשו ב-unknown במקום any

any מכבה את בדיקת הטיפוסים לערך ולכל מה שמחושב ממנו. גם unknown מקבל כל ערך, אבל חייבים לבדוק אותו לפני שמשתמשים בו, וזה שם את הבדיקה במקום שבו המידע נכנס לתוכנית:

הגבולות הם המקומות שבהם הטיפוסים כבר לא מובטחים: JSON.parse, תשובות של fetch, localStorage, קלט מטפסים, משתני סביבה והודעות מתהליכים אחרים. בצעו שם ולידציה עם type guard או עם ספריית סכמות, ושאר הקוד יוכל לסמוך על הטיפוסים שלו.

השאירו את strict פעיל

strict הוא ברירת המחדל ב-TypeScript 7; אל תכבו אותו. הוסיפו את הבדיקות שהוא משאיר בחוץ ושתופסות הכי הרבה באגים:

{
    "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitOverride": true,
        "noFallthroughCasesInSwitch": true
    }
}

noUncheckedIndexedAccess גורם ל-arr[i] ול-record[key] לכלול undefined, כי זה מה שהם מחזירים לאינדקס חסר. noImplicitOverride מחייב מתודה של תת-מחלקה לכתוב override, ו-noFallthroughCasesInSwitch דוחה case שממשיך ישר לזה שאחריו.

תנו להסקת הטיפוסים לעבוד

תנו הערות טיפוס למה ש-TypeScript לא יכולה לדעת: פרמטרים של פונקציות, וטיפוסי ההחזרה של פונקציות שמודולים אחרים משתמשים בהן. השאירו משתנים מקומיים ופרמטרים של callbacks להסקה. הערת טיפוס מיותרת היא לא רק רעש; היא יכולה להפוך טיפוס לרחב יותר מהערך:

בלי ההערה @ts-expect-error, הקריאה setStatus(annotated) היא השגיאה TS2345. ה-const שהטיפוס שלו הוסק שומר על טיפוס הליטרל "active", ולכן הוא מתקבל. רחפו מעל משתנה בעורך כדי לראות מה הוסק לפני שאתם מוסיפים טיפוס.

העדיפו union types על פני enums

union של ליטרלים של מחרוזות נותן השלמה אוטומטית ובדיקות ממצות בלי קוד שנוצר. כשצריך גם את רשימת הערכים בזמן ריצה, גזרו את הטיפוס ממערך as const:

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

function canEdit(role: Role): boolean {
  return role !== "viewer";
}

console.log(canEdit("editor"));     // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]

canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.

enum Role { Admin, Viewer } מתקמפל לאובייקט עם מיפוי הפוך, ופרמטר של enum מספרי מקבל כל משתנה number, גם כזה שמחזיק ערך שה-enum לא מפרט. בנוסף, enums לא יכולים לרוץ תחת type stripping של Node (TypeScript enum is not supported in strip-only mode). השיקולים מושווים בעמוד enums.

בדקו אובייקטי קונפיגורציה עם satisfies

הערת טיפוס רחב כמו Record<string, Route> על אובייקט בודקת את הערכים שלו אבל שוכחת את המפתחות. satisfies בודק את אותו הדבר ושומר את הטיפוס המדויק:

השתמשו בהערת טיפוס כשהמשתנה צריך בדיוק את הטיפוס המוצהר (פרמטר של פונקציה, ערך שתשימו לו ערך מחדש). השתמשו ב-satisfies לטבלאות חיפוש, מפות של routes, טוקנים של ערכות עיצוב ושאר אובייקטים קבועים.

תארו מצב עם discriminated unions

אובייקט יחיד עם שדות אופציונליים מאפשר מצבים שלא יכולים לקרות: loading: true יחד עם error, או data חסר אחרי הצלחה. union של אובייקטים עם תגית משותפת מאפשר רק את המצבים האמיתיים, וכל ענף רואה רק את השדות שלו:

השורה עם never היא הבדיקה הממצה. כשמישהו מוסיף מצב ושוכח לטפל בו, ה-build נשבר בשורה הזו:

השגיאה היא index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. היא מציינת את המקרה שלא טופל. הוסיפו case "cancelled": והקוד יעבור קומפילציה.

הימנעו מ-! ומ-as

ה-non-null assertion x! וה-type assertion x as T אומרים לקומפיילר להפסיק לבדוק. אף אחד מהם לא משנה את הערך בזמן ריצה, ולכן assertion שגוי הופך מאוחר יותר לקריסה, רחוק מהסיבה שלה:

החליפו את ! ב-?., ב-??, ב-return מוקדם, או בזריקת שגיאה עם הודעה מועילה. החליפו את as ב-type guard שבאמת בודק את הערך. ה-assertion היחיד שתמיד בטוח הוא as const, כי הוא רק הופך טיפוס לצר יותר ולקריאה בלבד. הגדרת lint טובה (no-non-null-assertion ו-no-explicit-any של typescript-eslint) מסמנת את השאר.

הפכו מידע ל-readonly

סמנו מאפיינים ומערכים כ-readonly כשקוד לא אמור לשנות אותם. אז הקומפיילר דוחה push, sort והשמות, ופונקציות מחזירות ערכים חדשים במקום לשנות את הקלט שלהן:

readonly נבדק רק בזמן קומפילציה ורק ברמה אחת לעומק: הוא לא מקפיא את האובייקט בזמן ריצה. זה עדיין מספיק כדי לתפוס את השינוי המקרי של מצב משותף שגורם לרוב הבאגים האלה.

שאלות נפוצות

האם כדאי להשתמש ב-any ב-TypeScript?

כמעט אף פעם בקוד של אפליקציה. any מכבה את הבדיקה לערך ולכל מה שנגזר ממנו. השתמשו ב-unknown לערכים שהטיפוס שלהם עדיין לא ידוע, וצמצמו אותם עם בדיקות; השאירו את any לפתחי מילוט נדירים, עם הערה שמסבירה למה.

האם צריך לתת הערת טיפוס לכל משתנה ב-TypeScript?

לא. תנו ל-TypeScript להסיק משתנים מקומיים ופרמטרים של callbacks. תנו הערות טיפוס לפרמטרים של פונקציות (אי אפשר להסיק אותם), ולטיפוסי ההחזרה של פונקציות מיוצאות, כדי ששינוי בתוך הפונקציה לא ישנה בשקט את הטיפוס הציבורי שלה.

האם enums ב-TypeScript הם הרגל רע?

לא שגוי, אבל צוותים רבים נמנעים מהם. enum מייצר קוד בזמן ריצה, לא יכול לרוץ תחת type stripping של Node, ופרמטר של enum מספרי מקבל כל משתנה number, לא משנה איזה ערך יש בו. union של ליטרלים של מחרוזות, או מערך as const עם טיפוס נגזר, נותנים את אותה השלמה אוטומטית ואותן בדיקות בלי קוד שנוצר.

מתי להשתמש ב-type assertions עם as?

רק כשאתם יודעים משהו שהקומפיילר לא יכול לדעת, ורצוי מיד אחרי בדיקה שמוכיחה את זה. as לא משנה שום ערך בזמן ריצה, ולכן {} as User עובר קומפילציה ואז אין לו name. ברוב המקרים, type guard שבודק את הערך הוא הכלי הבטוח יותר.

אילו הגדרות tsconfig הכי מתאימות לפרויקט TypeScript חדש?

השאירו את strict פעיל (ברירת המחדל ב-TypeScript 7) והוסיפו noUncheckedIndexedAccess. פרויקטים רבים מפעילים גם noImplicitOverride, noFallthroughCasesInSwitch ו-verbatimModuleSyntax. הקונפיגורציה ש-tsc --init כותב מגדירה, בין השאר, את strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes ו-verbatimModuleSyntax.

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

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

להתחיל