בואו נתקן את JSON.parse ב TypeScript

12/09/2022

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

הבעיה שגילוי מה לעשות בזמן ריצה הולך נגד הרעיון של Type Safety - כי אם אני לא יודע מה יש לי ביד, איך אני יודע שמותר לי לעשות איתו את מה שרציתי לעשות.

לדוגמה נניח שאני קורא אוביקט מ local storage ואני חושב שהוא מכיל מחרוזת שהיא ייצוג JSON-י של אוביקט שיש בו שני שדות מספריים עם המפתחות x ו y. הקוד הבא ב JavaScript קורא את האוביקט הזה ומדפיס את סכום המספרים:

const stringifiedValue = localStorage.getItem('value');
const parsedValue = JSON.parse(stringifiedValue);
const sum = parsedValue.x + parsedValue.y;

console.log(`sum = ${sum}`);

אבל כשאדון טייפסקריפט קורא את הקוד הזה הוא חוטף חום: איך אתה יודע, הוא צועק עליי, שבאמת יש ב local storage אוביקט עם המפתח שכתבת? ואיך אתה יודע שהערך הוא באמת מחרוזת שתצליח לתרגם אותה ל JSON? ואם כבר פיענחת אותה ל JSON, איך אתה יודע שלאוביקט שקיבלת יש שדות בשם x ו y והם מספרים?

בתרגום ל TypeScript אני צריך להרגיע את כל הדאגות. ניסיון ראשון שלא עובד עשוי להיראות כך:

type TwoNumbers = { x: number, y: number };

const stringifiedValue = localStorage.getItem('value');
const parsedValue: TwoNumbers = JSON.parse(stringifiedValue);
const sum = parsedValue.x + parsedValue.y;

console.log(`sum = ${sum}`);

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

import './style.css'

type TwoNumbers = { x: number, y: number };

const stringifiedValue = localStorage.getItem('value');
if (stringifiedValue) {
  const parsedValue: TwoNumbers = JSON.parse(stringifiedValue);
  const sum = parsedValue.x + parsedValue.y;

  console.log(`sum = ${sum}`);
}

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

עבודה חכמה יותר עם טיפוסים תכריח אותנו לבדוק מה באמת פירססנו ושהוא באמת מתאים לטיפוס שלנו. בשביל זה הכי קל לעטוף את JSON.parse:

import './style.css'

type TwoNumbers = { x: number, y: number };

function safeJsonParse(text: string): unknown {
  return JSON.parse(text)
}

const stringifiedValue = localStorage.getItem('value');
if (stringifiedValue) {
  const parsedValue: TwoNumbers = safeJsonParse(stringifiedValue);
  const sum = parsedValue.x + parsedValue.y;

  console.log(`sum = ${sum}`);
}

כבר התקדמנו! עכשיו טייפסקריפט כבר כועס על ההשמה מ JSON.parse ל parsedValue בגלל שלא בדקנו שהאוביקטים מתאימים. בשביל הבדיקה אני יכול להגדיר פונקציה נפרדת:


type TwoNumbers = { x: number, y: number };

function safeJsonParse(text: string): unknown {
  return JSON.parse(text)
}

function isTwoNumbers(value: any): value is TwoNumbers {
  return (
    'x' in value &&
    typeof value.x === 'number' &&
    'y' in value &&
    typeof value.y === 'number'
  )
}

const stringifiedValue = localStorage.getItem('value');
if (stringifiedValue) {
  const parsedValue = safeJsonParse(stringifiedValue);
  if (isTwoNumbers(parsedValue)) {
    const sum = parsedValue.x + parsedValue.y;
    console.log(`sum = ${sum}`);
  }
}

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