• בלוג
  • בואו נכתוב את maxBy ב TypeScript

בואו נכתוב את maxBy ב TypeScript

14/04/2024

הפונקציה maxBy היתה יכולה להיות יופי של תוספת ל JavaScript ו TypeScript אבל מכל מיני סיבות לא נכללה בסטנדרט. בואו נראה איך לתקן את הבעיה עם reduce בצורה ידידותית ל TypeScript.

1. חתימה

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

function maxBy<T>(data: Array<T>, key: (t: T) => number): T { ... }

אני מדמיין שאני מפעיל אותה באופן הבא:

const data = [
  { name: "Leela",  age: 31   },
  { name: "Fry",    age: 1031 },
  { name: "Hubert", age: 165  },
  { name: "Bender", age: 10   }
];

console.log(maxBy(data, d => d.age));

ומקבל את Fry שגילו 1031.

2. מימוש

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

function maxBy<T>(data: Array<T>, key: (t: T) => number): T {
  if (data.length === 0) {
    throw new Error("Array is empty");
  }

  return data
    .map(i => ({ key: key(i), value: i }))
    .reduce((max, value) => max.key > value.key ? max : value)
    .value
}

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

3. למה לשים לב

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

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

function maxBy<T>(data: Array<T>, key: (t: T) => number): T {
  return data
    .reduce((max, value) => key(max) > key(value) ? max : value)
}

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

function maxBy<T>(data: Array<T>, key: (t: T) => number): T {
  if (data.length === 0) {
    throw new Error("Array is empty");
  }

  return data
    .map(i => [i, key(i)])
    .reduce((max, value) => max[1] > value[1] ? max : value)
    [0] as T
}