JSON Toolkit

JSON ל-TypeScript: ממשקים, type guards והגבולות של היצירה האוטומטית

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

בינוניכ-9 דקות קריאהפורסם 2026-10-31עודכן 2026-11-04
תמונה עבור המדריך JSON ל-TypeScript interface ו-type guard

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

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

1. מה המחולל באמת עושה

הדביקו דוגמת JSON בJSON ל-TypeScript וה-interface שתקבלו בחזרה הוא תקציר צורה: הוא סופר את המפתחות ואת טיפוסי הערכים שראה, ואז בוחר את הייצוג הצר ביותר הזמין.

{
  "id": 1,
  "email": "ada@example.com",
  "profile": { "nickname": "Ada", "age": 36 },
  "tags": ["admin", "beta"],
  "createdAt": "2026-10-31T08:00:00Z"
}

הפלט נראה בערך כך:

interface User {
  id: number;
  email: string;
  profile: { nickname: string; age: number };
  tags: string[];
  createdAt: string;
}

זה מתקמפל, אבל זה מתאר ערך אידיאלי, לא את החוזה האמיתי של נקודת הקצה. המחולל אינו יודע אם email יכול להיעדר, אם tags יכול להיות ריק, או אם createdAt תמיד נושא אזור זמן. הוא אפילו לא יודע ש-id אולי מחרוזת ברשומות אחרות.

לכן הציפייה הנכונה היא: המחולל חוסך הקלדה, האדם מספק משמעות.

2. שדות אופציונליים ו-null: ? אינו | null

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

צורה ב-TypeScript המשמעות JSON תואם
email: string תמיד קיים, תמיד מחרוזת {"email":"a@b.c"}
email?: string המפתח עשוי להיעדר; כשהוא קיים הוא לא יכול להיות null {} או {"email":"a@b.c"}
email: string | null המפתח תמיד קיים, הערך עשוי להיות null {"email":null}
email?: string | null המפתח עשוי להיעדר והערך עשוי להיות null כל שלושת המקרים

המחולל ראה דוגמה אחת, ולכן הוא בדרך כלל פולט את הצורה הראשונה. אם ה-API באמת יכול להחזיר null, הטיפוס משקר: הכול מתקמפל, ואז email.toLowerCase() זורק בזמן ריצה.

יש שתי דרכים לתקן זאת:

  1. אספו כמה דוגמאות אמיתיות שמכסות גם "המפתח חסר" וגם "הערך הוא null", ואז תנו למחולל להסיק מחדש.
  2. אחרי היצירה, שנו שדות ביד ל-?: T | null וטפלו באמת בשני הענפים בקוד.

אל תקחו את הקיצור של סימון הכול כ-?: string. אופציונלי משמעו "המפתח אולי לא קיים", לא "הערך אולי ריק". גם ל-undefined ול-null יש התנהגות שונה ב-JSON.stringify: הראשון מושמט, השני נשמר כ-null.

3. איברי מערך לא אחידים וטיפוסי איחוד

מערכים הם המבנה הקל ביותר לטעות בו. אם tags מופיע כ-["admin"], כ-[] וכ-["beta", 3] בשלוש רשומות, המחולל עשוי לפלוט string[], (string | number)[] או unknown[], בהתאם לדוגמאות שראה.

אדם חייב להחליט:

  • טיפוסי האיברים באמת מעורבים: כתבו (string | number)[] וצמצמו בנקודת השימוש.
  • רק רשומה אחת מלוכלכת: תקנו את הנתונים. אל תסתירו זאת עם any[].
  • מערכים ריקים: המחולל אינו יכול להסיק טיפוס איבר ונופל חזרה ל-never[] או any[], ולכן חייבים למלא אותו.
  • טאפלים באורך קבוע: השתמשו ב-[string, string] רק כשהעסק באמת מבטיח את האורך; אחרת הוספת איבר בהמשך הופכת לשגיאת קומפילציה.

גם מערכים של אובייקטים זקוקים לתשומת לב כשהמפתחות לא אחידים:

interface OrderItem {
  sku: string;
  quantity: number;
  discount?: number; // ברשומות מסוימות השדה הזה לא קיים
}

שימו אופציונליות על טיפוס האיבר, לא על המערך. items?: OrderItem[] משמעו שהמערך עצמו עשוי להיעדר, וזו הצהרה שונה לגמרי.

4. תאריכים, שלמים גדולים, enums: טיפוסים של-JSON אין

ל-JSON יש רק מחרוזות ומספרים; המשמעות העסקית חיה במוסכמות. המחולל מסיק מ-literal, ולכן תקנו ביד:

literal ב-JSON פלט נוצר אופייני מה זה צריך להיות
"2026-10-31T08:00:00Z" string השאירו string בשכבת התעבורה והמירו בנקודת השימוש; לעולם אל תצהירו Date ישירות
1717075200000 number השתמשו ב-number לחותמת זמן במילישניות ותעדו את היחידה
"1234567890123456789" string השאירו אותו string כדי ש-number לא יאבד דיוק
1 ו-"1" מעורבים number | string נרמלו לצורה אחת; אל תדחפו את האיחוד לקוד העסקי
"admin" string איחוד literal 'admin' | 'editor', או קבוע enum משותף
true ו-"true" מעורבים boolean | string תקנו את מקור הנתונים במקום ליצור את הטיפוס הזה

הכלל החשוב ביותר: תאריכים ב-JSON הם תמיד מחרוזות. כתיבת createdAt: Date היא הונאה עצמית נפוצה — JSON.parse לעולם לא בונה Date, ולכן הערך בזמן ריצה נשאר מחרוזת ו-.getTime() זורק. השאירו את טיפוס התעבורה כ-string, קראו במפורש ל-new Date(value) בגבול, ובדקו Number.isNaN.

שלמים גדולים עובדים באותו אופן. ברגע ש-id עובר 2^53, JSON.parse כבר איבד דיוק, ויצירת number רק מבטיחה את הבאג בתוך הטיפוס. השרת מחזיר מחרוזת, הצד הלקוח משאיר אותה מחרוזת; זו האפשרות האמינה היחידה.

5. טיפוסים שנוצרו אינם מבצעים אימות בזמן ריצה

טיפוסי TypeScript נמחקים בזמן קומפילציה. לא משנה כמה ה-interface מדויק, הוא אינו יכול לעצור תגובה של {"id": "x"} — הוא מגביל את הקוד שלכם, לא את הבתים המגיעים דרך הרשת.

/**
 * בודק אם ערך בזמן ריצה הוא אובייקט משתמש.
 * @param value הערך בזמן ריצה שיש לאמת
 * @returns האם הוא עומד בצורה המינימלית של User
 */
function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) {
    return false;
  }
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === 'number' && typeof candidate.email === 'string';
}

type guard מכסה רק את השדות שכתבתם בבדיקה. ככל שהשדות מתרבים, guards שנכתבו ביד הופכים להגדרה שנייה לתחזוקה, שנסחפת הרחק מה-interface.

הפשרה המעשית נראית כך:

גישה אימות בזמן ריצה מקור הטיפוסים מתאים ל
interface בלבד אין נכתב ביד או נוצר נתונים פנימיים מהימנים, אילוצים בזמן קומפילציה
interface בתוספת guard שנכתב ביד כן שתי הגדרות שנסחפות זו מזו מעט שדות, גבול ברור
zod / valibot כן טיפוסים מוסקים מה-schema APIs חיצוניים, טפסים, תצורה
JSON Schema בתוספת מאמת כן טיפוסים נוצרים מה-schema חוזים חוצי-שפות, תיעוד

אם הנתונים מגיעים מבחוץ, הפכו את ה-schema למקור האמת היחיד וגזרו טיפוסים עם z.infer או json-schema-to-ts. אז "האימות עבר" ו"הטיפוס נכון" הם אותה עובדה, ואין הגדרה שנייה שתיסחף. לגישת הסנכרון הדו-כיווני המלאה, ראו סנכרון בין Schema לטיפוסי TypeScript.

6. Interface או type: אל תתווכחו על הפלט הנוצר

מחוללים בדרך כלל פולטים interface, אבל השניים ניתנים להחלפה ברוב המצבים. מה שבאמת חשוב הוא שלוש יכולות:

  • מיזוג הצהרות (declaration merging): interface ניתן להצהיר כמה פעמים והוא מתמזג אוטומטית; type לא. זה עוזר בתיקון טיפוסים של צד שלישי, אבל מודלים עסקיים צריכים להימנע מכך — המיזוג מאפשר ל"שדות עודפים" לחמוק מבעד למהדר בלי שמבחינים.
  • איחודים וטיפוסים ממופים: type יכול לבטא ישירות A | B, Partial<T> וטיפוסי template literal; interface לא.
  • רקורסיה: שניהם תומכים בה, אבל צורות רקורסיביות מורכבות נקראות באופן טבעי יותר כ-type.

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

7. שרשרת הכלים: שלוש דרכים ליצור

גישה קלט חוזק מגבלה
JSON ל-TypeScript של האתר דוגמת JSON עובד מיד, מעובד מקומית מבוסס רק על הדוגמה הנוכחית; דורש צמצום ידני
quicktype JSON / JSON Schema פלט רב-לשוני, שילוב ב-CLI הסקה שמרנית, איחודים יכולים להיות רחבים מדי
json-schema-to-typescript JSON Schema מונחה חוזה וניתן לשחזור דורש קודם schema אמין

קריאה אופיינית ל-quicktype:

npx quicktype -s json -o user.ts user.json --just-types

--just-types פולט רק טיפוסים ובלי קוד המרה בזמן ריצה, מה שמתאים לפרויקטים שכבר יש להם שכבת פענוח. אם ה-schema נכתב ביד או נוצר על ידי ה-backend, json-schema-to-typescript מתאים יותר מפני שהוא מכבד את required, enum ו-additionalProperties — אילוצים שהסקה מדוגמה פשוט אינה יכולה לראות.

ליצירת schema יש מלכודות משלה, כמו סימון כל שדה כ-required או הפיכה שקטה של null לאופציונלי; ראו מלכודות נפוצות במחוללי JSON Schema.

8. רשימת בדיקה לפני commit של טיפוסים שנוצרו

עברו על חמשת הפריטים האלה לפני שאתם מכניסים את הפלט ל-commit:

  1. נרמלו שמות שדות: אל תתנו ל-userId ול-user_id להתקיים יחד; מפו פעם אחת בשכבת המרה.
  2. בדקו מחדש אופציונליות: השוו לתגובות אמיתיות ומצאו אילו שדות יכולים להיעדר או להיות null.
  3. השאירו תאריכים ו-ID כמחרוזות: תעדו את היחידה והפורמט בטיפוס, ופענחו במפורש בנקודת השימוש.
  4. צמצמו any ו-unknown: בכל מקום שבו המחולל פלט any, או הוסיפו טיפוס או השאירו unknown והוסיפו guard.
  5. הוסיפו אימות בזמן ריצה: אמתו נתונים חיצוניים לפחות פעם אחת; אל תצפו ש-interface יתפוס נתונים רעים.

אם הפרויקט משתמש בכמה ספריות JSON (JSON.parse, superjson, lossless-json), ההתנהגות שלהן עם שלמים גדולים ותאריכים שונה, ולכן אשרו אותה בזמן הבחירה. ראו בחירת ספריית פענוח JSON.

למה המחולל מסמן כל שדה אופציונלי כ-required?

הוא ראה דוגמה אחת, ולכן כל מפתח שהופיע מטופל כקיים תמיד. ספקו כמה דוגמאות שמכסות מקרים שונים, או צרו מ-JSON Schema — מערך ה-required שם הוא המקור הסמכותי.

למה הצהרת Date ב-interface נכשלת בזמן ריצה?

JSON.parse מפיק רק מחרוזות, מספרים, בוליאניים, null, אובייקטים ומערכים; הוא לעולם לא בונה Date. הצהרת Date היא הבטחה בזמן קומפילציה בעוד הערך בזמן ריצה נשאר מחרוזת. השאירו string בשכבת התעבורה והמירו בגבול.

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

לא. טיפוסי TypeScript נמחקים אחרי הקומפילציה ואינם מטילים דבר על נתונים בזמן ריצה. אמתו נתונים חיצוניים עם zod, valibot, ajv או type guard שנכתב ביד.

id של מספר שלם גדול צריך להיות מטופס number או string?

אם הוא יכול לעבור 2^53, הוא חייב להיות string. JSON.parse מאבד דיוק בזמן הפענוח, וטיפוסו כ-number רק כותב את הבאג לתוך הטיפוס, במקום שבו לא ניתן עוד לתקן אותו.