שיטות עבודה מומלצות

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

1. strict תמיד

פרויקט בלי "strict": true מפספס את רוב הערך, בעיקר את בדיקת null. בפרויקט ישן שעובר ל-TypeScript אפשר להדליק בהדרגה, אבל היעד הוא strict מלא.

2. בלי any

// ❌ any: הבדיקה כבויה, ובאג מתפשט לכל מקום שהערך מגיע אליו
function handle(data: any) { return data.user.name; }

// ✅ unknown: חייבים לבדוק לפני שימוש
function handle(data: unknown) { /* בדיקה, ואז שימוש */ }

ההגדרה noImplicitAny (חלק מ-strict) מונעת any שנוצר בטעות. כש-any נחוץ באמת, הוסיפו הערה למה.

3. לבדוק בגבולות, לסמוך בפנים

טיפוסים לא קיימים בזמן ריצה. כל מה שנכנס מבחוץ: תשובת API, req.body, localStorage, JSON.parse, משתני סביבה, צריך בדיקה אמיתית בנקודת הכניסה:

const productSchema = z.object({ id: z.number(), name: z.string(), price: z.number() });
type Product = z.infer<typeof productSchema>;

async function getProducts(): Promise<Product[]> {
    const response = await fetch('/api/products');
    return z.array(productSchema).parse(await response.json());   // בדיקה, לא הבטחה
}

אחרי שהנתון עבר את הגבול, שאר הקוד יכול לסמוך על הטיפוס בלי לבדוק שוב.

4. להימנע מ-as ומ-!

const user = data as User;        // ❌ הבטחה בלי בדיקה
const name = user!.name;          // ❌ "אני בטוח שזה לא null"

if (isUser(data)) { /* ... */ }   // ✅ Type Guard
const name = user?.name ?? 'אורח'; // ✅ טיפול במקרה החסר

5. לתת להסקה לעבוד

// ❌ רעש
const count: number = items.length;
const names: string[] = users.map((user: User): string => user.name);

// ✅ הטיפוסים זהים, הקוד נקי
const count = items.length;
const names = users.map((user) => user.name);

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

6. לתאר מצבים, לא דגלים

// ❌ אפשר להיות גם loading וגם error. data תמיד "אולי"
type State = { loading: boolean; error?: string; data?: Product[] };

// ✅ Discriminated Union: רק מצבים שקיימים באמת
type State =
    | { status: 'loading' }
    | { status: 'error'; error: string }
    | { status: 'success'; data: Product[] };

7. לגזור טיפוסים, לא להעתיק

type User = { id: number; name: string; email: string; passwordHash: string };

type PublicUser = Omit<User, 'passwordHash'>;   // ✅ מתעדכן לבד
type UserId = User['id'];                          // ✅ number, מקור אחד

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

8. בדיקת טיפוסים ב-CI

Vite ו-Node.js רק מסירים טיפוסים ולא בודקים אותם, כך שקוד עם שגיאות טיפוס ירוץ בשקט. tsc --noEmit חייב לרוץ לפני כל מיזוג, יחד עם הבדיקות ו-lint:

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "lint": "eslint .",
    "test": "node --test",
    "check": "npm run typecheck && npm run lint && npm test"
  }
}

ל-lint מומלץ typescript-eslint, שמוסיף כללים שמבינים טיפוסים (למשל Promise ששכחתם לחכות לו).

מה הלאה

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

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. איפה חייבים בדיקה בזמן ריצה?

    1. אף פעם, TypeScript מספיקה
    2. בגבולות המערכת: קלט מבחוץ כמו API, טפסים ו-JSON
    3. בכל פונקציה
    4. רק בבדיקות
    הצגת התשובה

    תשובה ב. אחרי שהנתון עבר את הגבול, שאר הקוד יכול לסמוך על הטיפוס.

  2. למה להריץ tsc --noEmit ב-CI?

    1. כדי ליצור קבצים
    2. כדי להאיץ את האתר
    3. זה לא נחוץ
    4. כי Vite ו-Node.js רק מסירים טיפוסים, וקוד עם שגיאות טיפוס ירוץ בשקט
    הצגת התשובה

    תשובה ד. הבדיקה היא השלב היחיד שבאמת אוכף את הטיפוסים.