JSON ל-TypeScript

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

קלט
TypeScript
export interface Root {
  id: number
  name: string
  active: boolean
  address: Address
  roles: string[]
  posts: Post[]
  tags: unknown[]
}

export interface Address {
  city: string
  zip: null
}

export interface Post {
  id: number
  title: string
  views: number | string
  pinned?: boolean
}

ממשקים: 3

טיפוסים מוסקים מהנתונים, לא מנוחשים

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

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

כיצד ממוזגים מערכים

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

  • מפתח שקיים בחלק מהאיברים אך חסר באחרים הופך לאופציונלי, ונכתב עם סימן שאלה. אם למחצית מהרשומות שלכם יש "middleName" ולמחצית אין, השדה הוא "middleName?", וזה בדיוק מה שצרכן צריך לטפל בו.
  • מפתח שערכו שונה בסוגו בין האיברים הופך ל-union. שדה שהוא מספר ברשומה אחת ומחרוזת באחרת מקבל טיפוס "number | string" — לא מפני שזה מסודר, אלא מפני שזה מה שהנתונים באמת מכילים ומה שהקוד שלכם צריך לקבל.
  • אובייקט מקונן בתוך האיברים ממוזג באותו אופן, רקורסיבית, ומחולץ לממשק משלו. עשרה איברי מערך שכל אחד נושא "address" מפיקים ממשק Address אחד המתאר את כל העשרה.

כשהמסמך עצמו הוא מערך ברמה העליונה, השורש הופך לכינוי לממשק פריט — למשל "type Root = RootItem[]" — עם טיפוס האיבר הממוזג כתוב מתחתיו.

null, אופציונלי, ולמה הם שונים

מפתה להתייחס ל-null כמו למפתח חסר, וזה שגוי. ב-TypeScript "name?: string" אומר שהמאפיין עשוי להיעדר; "name: string | null" אומר שהוא תמיד שם אך עשוי להחזיק null. אלו חוזים שונים, וצרכן בודק אותם אחרת — "in" מול השוואת ערך. הכלי הזה שומר עליהם נפרדים: null מפורש בנתונים הופך לאיבר union "| null", ממוקם אחרון כך ש-"string | null" נקרא כפי שאתם מצפים, בעוד מפתח שפשוט נעדר מחלק מהרשומות הופך לאופציונלי. שדה שהוא גם וגם — null ברשומה אחת, חסר באחרת — יוצא כשניהם, "field?: T | null", מפני ששתי העובדות נכונות לגבי הנתונים שלכם.

אובייקטים מקוננים הופכים לממשקים בעלי שם

במקום להטמיע צורה מקוננת בתוך ההורה שלה, כל אובייקט מחולץ לממשק משלו ששמו נגזר מהמפתח שתחתיו הוא יושב. אובייקט "user" הופך לממשק User; "address" בתוכו הופך לממשק Address ש-User מפנה אליו. טיפוסים מוטמעים לעומק קשים לקריאה ובלתי-אפשריים לשימוש חוזר, וממשקים בעלי שם הם מה שהייתם כותבים בעצמכם. איברי מערך מוצגים ביחיד כשאפשר — "users" מפיק User, "categories" מפיק Category — ומפתח שאינו נוטה לרבים מקבל סיומת Item כך שלאיבר עדיין יש שם משלו.

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

נפילת ה-unknown

חלק מהערכים אינם נושאים מידע טיפוס כלל. מערך ריק יכול להחזיק כל דבר; לאובייקט ריק אין מפתחות לתאר. במקום לפנות ל-"any" — שמכבה בדיקת טיפוסים לכל מה שבהמשך — הכלי נופל ל-"unknown": מערך ריק הופך ל-"unknown[]", אובייקט ריק הופך ל-"Record<string, unknown>". ההבדל חשוב. "any" מעביר באגים בשקט; "unknown" מכריח את הצרכן לצמצם את הערך לפני השימוש בו, כך שהטיפוס המוסק נשאר כן לגבי מה שהדוגמה סיפרה ומה שלא.

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

על מה הוא רץ, ואיפה

הכול קורה בדפדפן שלכם. ה-JSON מנותח והטיפוסים מוסקים על המכשיר שלכם; שום דבר שתדביקו לא מועלה, נשמר או נרשם. זה הופך את הכלי לבטוח לשימוש על תגובת API אמיתית או קובץ הגדרות עם סודות בתוכו — הדוגמה לעולם לא עוזבת את הדף. הפלט הוא TypeScript רגיל שאפשר להדביק ישירות לקובץ "d.ts" או למודול, להתאים את מעט שדות ה-"unknown" שהנתונים לא הצליחו לתאר, ולהשתמש.

שאלות נפוצות

האם ה-JSON שלי נשלח לשרת?
לא. המסמך מנותח והטיפוסים מוסקים כולם בדפדפן שלכם, ושום דבר שתדביקו לא מועלה או נרשם. בטוח לשימוש על תגובת API אמיתית או קובץ הגדרות.
למה שדה אופציונלי כשהוא קיים בדוגמה שלי?
מפני שהוא נעדר מלפחות איבר אחד של מערך שהכלי מיזג. ההסקה קוראת כל איבר, לא רק את הראשון, כך שמפתח שחלק מהרשומות משמיטות הופך לאופציונלי — זה הטיפוס שהנתונים שלכם באמת תומכים בו, גם אם הרשומה שהסתכלתם עליה במקרה כללה אותו.
למה שדה הפך ל-union כמו string | number?
מפני שלערך היו סוגים שונים באיברי מערך שונים — מחרוזת ברשומה אחת ומספר באחרת. הטיפוס הממוזג חייב לקבל את שניהם, אז הוא נכתב כ-union. אם זה מפתיע אתכם, זה בדרך כלל אומר שהנתונים פחות אחידים ממה שציפיתם, וכדאי לדעת זאת.
למה הכלי משתמש ב-unknown ולא ב-any?
עבור ערכים שהוא לא יכול לתאר — מערך ריק, אובייקט ריק — "unknown" שומר על התוצאה בטוחת-טיפוסים, ומכריח צרכן לצמצם את הערך לפני השימוש בו, בעוד "any" היה מכבה בדיקת טיפוסים. אפשר להדק את השדות האלה ביד ברגע שתדעו מה האוסף הריק מחזיק.
מה ההבדל בין פלט interface לפלט type?
אין הבדל בטיפוסים שהם מתארים — שניהם מפיקים את אותן צורות. "interface" הוא הניב הנפוץ לטיפוסי אובייקט וניתן להרחבה ולמיזוג; כינויי "type" הם מה שכמה בסיסי קוד מעדיפים להשתמש בהם לאורך כל הדרך. המתג קיים כדי שהפלט יתאים לסגנון הפרויקט שלכם.
האם הוא יכול להפוך TypeScript בחזרה ל-JSON?
לא, ובמכוון. ההמרה היא חד-כיוונית: JSON נכנס, טיפוסים יוצאים. הכיוון ההפוך היה כרוך בהמצאת ערכים שמעולם לא היו בנתונים שלכם, וכל העניין הוא שכל הצהרה נגזרת ממה שבאמת הדבקתם.
איך ממשקים מקוננים מקבלים שם?
מהמפתח שתחתיו הם יושבים: אובייקט "user" הופך ל-User, "address" בתוכו הופך ל-Address. איברי מערך מוצגים ביחיד כשאפשר — "categories" נותן Category — ומפתח שאינו נוטה לרבים מקבל סיומת Item. שתי צורות שונות שהיו מתנגשות בשם מקבלות סיומת ולא ממוזגות, כך שהן נשארות נבדלות.