• בלוג
  • משחקים עם טייפסקריפט - יצירת טיפוס לאינדקס חוקי במערך

משחקים עם טייפסקריפט - יצירת טיפוס לאינדקס חוקי במערך

13/12/2022

ניקח מערך של פריטים קבועים, למשל מערך של צבעים:

const colors = ['red', 'blue', 'green', 'white'];

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

function getColorName(index: number) {
    return colors[index];
}

וזה עובד כמובן אבל אנחנו לא נהנים מבדיקת הטיפוסים - אני יכול להפעיל את הפונקציה על אינדקסים חוקיים או לא חוקיים והכל יעבוד:

// compiles OK
getColorName(1);

// also compiles OK - but 129 is not a valid index
getColorName(129);

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

function getColorName(index: 0|1|2|3) {
    return colors[index];
}

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

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

type StringToNumber<T extends string, A extends any[] = []> =
  T extends keyof [0, ...A] ? A['length'] : StringToNumber<T, [0, ...A]>

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

ומשתמשים בה כדי לייצר טיפוסים באופן הבא:

// Two == 2
type Two = StringToNumber<'2'>;

// Three == 3
type Three = StringToNumber<'3'>;

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

type Error = StringToNumber<'hello'>;

עם השגיאה המופלאה:

Type instantiation is excessively deep and possibly infinite.(2589)

וזה מהמם כי אנחנו יכולים "לתפוס" את השגיאה ולהשתמש בה כדי לזרוק מפתחות מיותרים בעזרת Mapped Types. הרעיון הוא לקחת את כל המפתחות ב items, שזה כל האינדקסים (בתור מחרוזות) אבל גם המאפיין length וכל שאר הפונקציות של המערך, ולסנן מהם רק את אלה שאפשר להמיר למספר, ועל הדרך גם להמיר אותם למספרים. בסוף נפעיל keyof על התוצאה ונקבל את איחוד המספרים שחיפשנו. זה הקוד:

type ValidIndex = keyof {
    [i in keyof typeof items as (i extends string ? StringToNumber<i> : never)]: 2;
}

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

// Compiles OK - as 2 is a valid index in the array
const valid: ValidIndex = 2;

// Compilation error - 10 is not a valid index
const invalid: ValidIndex = 10;