JSON.parse מחזיר any, ולכן TypeScript מקבל כל טיפוס שתשימו בו את התוצאה. בזכות זה, מתן טיפוס ל-JSON מפוענח לוקח שורה אחת, אבל זה גם אומר שהטיפוס הוא הבטחה שלכם, ולא משהו שהקומפיילר בודק:
באובייקט השני age הוא המחרוזת "41". TypeScript עדיין מתייחס אליו כ-number, כי אפשר להשים any לכל דבר, והתוכנית מדפיסה 411. ל-JSON שהקוד שלכם כתב לפני רגע, ה-annotation מספיק. לנתונים מבקשת רשת, מקובץ או מ-local storage, אמתו אותם.
פענוח לתוך unknown
כשנותנים לתוצאה את הטיפוס unknown, הקומפיילר דורש בדיקה לפני שמשתמשים בכל מאפיין:
השגיאה היא index.ts(4,13): error TS18046: 'data' is of type 'unknown'. כל בדיקה שתכתבו אחר כך מצמצמת את data עוד קצת.
אימות עם Type Guard
type guard הוא פונקציה שמחזירה value is User. כשהיא מחזירה true, TypeScript מתייחס לערך כ-User מאותו רגע, והבדיקות שבתוכה הן בדיקות אמיתיות בזמן ריצה:
JSON.parse עצמו זורק SyntaxError על טקסט פגום, ולכן בקוד אמיתי עוטפים אותו גם ב-try/catch. ב-payloads גדולים או מקוננים, guards שנכתבים ביד מתארכים; ספריות סכמה כמו Zod או Valibot מאפשרות להצהיר על המבנה פעם אחת ולגזור ממנו גם את פונקציית האימות וגם את הטיפוס של TypeScript.
המרת JSON ל-interface של TypeScript
המרה של דוגמת JSON לטיפוסים היא עבודה מכנית. בהינתן התשובה הזו:
{
"id": 42,
"title": "Learn TypeScript",
"done": false,
"owner": { "id": 7, "name": "Ada" },
"tags": ["study", "ts"],
"dueDate": "2024-03-15T10:30:00Z",
"notes": null
}
התאימו לכל ערך את הטיפוס שלו, תנו לאובייקטים מקוננים interface משלהם, וסמנו את מה שיכול להשתנות:
דוגמה אחת לא יכולה לומר לכם אילו שדות הם אופציונליים או יכולים להיות null. בדקו כמה תשובות, או את התיעוד של ה-API, לפני שאתם מחליטים על ? ועל | null.
תאריכים וה-Reviver
ל-JSON אין טיפוס תאריך, ולכן תאריכים מגיעים כמחרוזות. הארגומנט השני של JSON.parse, ה-reviver, נקרא עבור כל מפתח ויכול לבנות אותם מחדש:
הפרמטר value של ה-reviver הוא any, וכך גם התוצאה, ולכן עדיין סומכים על הטיפוס Order ולא בודקים אותו. JSON.stringify(order, null, 2) מזיח את הפלט בשני רווחים ומחזיר את ה-Date למחרוזת ה-ISO שלו.
JSON.stringify ומה שהולך לאיבוד
ל-JSON.stringify יש טיפוס החזרה string. הערכים שהוא ממיר לא תמיד חוזרים אותו דבר, והטיפוס לא מזהיר אתכם:
| ערך | אחרי JSON.stringify |
|---|---|
Date | מחרוזת ISO (דרך המתודה toJSON שלו) |
Map, Set | {} (המירו קודם עם [...set] או Object.fromEntries(map)) |
undefined, פונקציות, symbols בתוך אובייקט | המפתח מושמט |
undefined, פונקציות, symbols בתוך מערך | null |
undefined, פונקציה או symbol לבד | undefined, לא מחרוזת |
NaN, Infinity | null |
bigint | זורק TypeError |
כללי זמן הריצה זהים לאלה של JavaScript רגיל, ומוסברים ב-JSON ב-JavaScript.
טיפוס לכל ערך JSON
כשקוד מטפל ב-JSON שרירותי, טיפוס רקורסיבי מתאר בדיוק מה JSON יכול להכיל, ודוחה ערכים שהוא לא יכול:
בלי ההערה, השורה האחרונה היא שגיאת קומפילציה, כי אובייקט Date אינו JsonValue.
ייבוא קובצי .json
אפשר לייבא קובץ .json כמו מודול, ו-TypeScript מסיק את הטיפוס שלו מהתוכן:
{ "name": "app", "port": 8080, "tags": ["a"] }
// CommonJS output, or a bundler
import config from "./config.json";
const port: number = config.port; // typed from the file: number
// An ES module under module: nodenext
import settings from "./config.json" with { type: "json" };
ב-TypeScript 7 זה עובד בלי הגדרות נוספות כש-module מוגדר ל-nodenext, node20, commonjs, esnext או preserve. תחת node16 ו-node18 זה נכשל עם השגיאה TS2732, Cannot find module './config.json'. Consider using '--resolveJsonModule' to import module with '.json' extension., עד שמוסיפים "resolveJsonModule": true; הגדרה שלו ל-false מכבה ייבוא JSON בכל מקום. ב-ES module תחת nodenext או node20, הייבוא צריך את ה-attribute with { type: "json" } (שגיאה TS1543 בלעדיו), ורק default import מותר (שגיאה TS1544 עבור import { port }). tsc מעתיק את קובץ ה-.json המיובא ל-outDir, ליד ה-JavaScript המקומפל.
שאלות נפוצות
איזה טיפוס JSON.parse מחזיר ב-TypeScript?
any. הקומפיילר לא יכול לדעת מה מחרוזת מכילה, ולכן const user: User = JSON.parse(text) עובר קומפילציה לא משנה מה יש בטקסט. כשהנתונים מגיעים מחוץ לתוכנית שלכם, השימו את התוצאה במשתנה מטיפוס unknown ואמתו אותה.
איך ממירים JSON ל-interface של TypeScript?
קחו דוגמה מייצגת וכתבו מאפיין אחד לכל מפתח: מחרוזות, מספרים ובוליאנים הופכים ל-string, ל-number ול-boolean, אובייקט מקונן מקבל interface משלו, מערך של אובייקטים הופך ל-Item[], ומפתחות שלפעמים חסרים מקבלים ?. מחוללי קוד כמו quicktype עושים את זה אוטומטית, אבל בדקו את הניחושים שלהם מול יותר מדוגמה אחת.
איך מייבאים קובץ JSON ב-TypeScript?
import config from "./config.json"; עובד ב-TypeScript 7 כש-module מוגדר ל-nodenext, node20, commonjs, esnext או preserve, והתוצאה מקבלת טיפוס לפי תוכן הקובץ. עם node16 או node18, הגדירו גם "resolveJsonModule": true. ב-ES module תחת nodenext או node20, הוסיפו את ה-attribute ש-Node דורש: import config from "./config.json" with { type: "json" };.
האם JSON.stringify תמיד מחזיר מחרוזת?
הטיפוס שלו אומר string, אבל JSON.stringify(undefined) ו-JSON.stringify(() => 1) מחזירים undefined בזמן ריצה. גם ערכים בתוך אובייקטים מומרים: Date הופך למחרוזת ISO, ו-Map ו-Set הופכים ל-{}.