Menu

JSON ב-TypeScript: טיפוס ל-JSON.parse, JSON ל-interface ו-imports

JSON.parse מחזיר any, ולכן TypeScript סומך על כל טיפוס שתתנו לתוצאה. כאן תלמדו לתת טיפוס ל-JSON מפוענח, לאמת אותו עם type guard, להפוך דוגמת JSON ל-interface, לייבא קובצי .json, ומה JSON.stringify עושה ל-Dates, ל-Sets ול-undefined.

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

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, Infinitynull
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 הופכים ל-{}.

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

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

להתחיל