למה ואיך להשתמש ב Generics ב TypeScript
אחד החיבורים שאני אוהב הוא בין TypeScript ל React Hooks - בצד של ריאקט המעבר ל Function Components הפך את הקוד להרבה יותר פשוט מאשר בתקופה שעבדנו עם מחלקות, ובצד של TypeScript הוא מצליח להבין כמעט את כל מה שאני זורק עליו והתמיכה בטיפוסים עוזרת לכתוב קוד יותר יציב.
עד שמגיעים לכתוב Custom Hooks.
ניקח לדוגמא את הקוד הבא שמגדיר Custom Hook שפונה לשרת להביא מידע בתור JSON:
function useRemoteData(endpoint: string, id: string) {
const [data, setData] = useState<any|null>(null);
useEffect(function() {
setData(null);
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const req = axios.get(`https://swapi.co/api/${endpoint}/${id}/`, {
cancelToken: source.token,
});
req.then(function(response) {
// when we get response
setData(response.data);
});
return function cancel() {
source.cancel();
}
// code continues here
}, [id]);
return data;
}
הקוד עובד אבל שימו לב לשורה שמגדירה את סוג המשתנה data:
const [data, setData] = useState<any|null>(null);
בגלל שבתוך ה Custom Hook אני לא יודע מה יהיה הנתיב בשרת שיבקשו ממני להביא, ומה מבנה המידע שצריך לחזור ממנו (כי ה Hook נמצא בשימוש של מספר קומפוננטות), לא ידעתי איזה סוג משתנה לבחור לערך שהשרת מחזיר.
הבעיה עם הקוד הזה היא שברירת המחדל של קוד חיצוני שמשתמש בו היא לעבוד בלי הגדרות טיפוסים ותוך הסתמכות על הטיפוס any שחוזר מהפונקציה. במילים אחרות הקוד הבא מתקמפל ועובד ולא בודק שגיאות כתיב או גישה לשדות שלא צריכים להיות בתשובת השרת:
function FilmInfo(props: { id: string }) {
const { id } = props;
// Get character data ???
const data = useRemoteData('films', id);
if (data === null) {
return <p>Loading, please wait...</p>
}
return (
<div>
<p>title: {data.title}</p>
<p>release_date: {data.release_date}</p>
<hr />
</div>
)
}
בהנחה שאני יודע לגבי כל נתיב מה הוא אמור להחזיר, דרך אחת לגרום לקוד שמשתמש ב Hook לוודא שהוא משתמש רק בשדות שהוא צריך היא להשתמש בפקודה as מתוך הקוד שקורא ל hook, כלומר להחליף את השורה השניה בפונקציה בשורה הזו:
const data = (useRemoteData('films', id) as IDataFilm);
ולהוסיף באיזשהו מקום בקובץ את הגדרת הממשק IDataFilm. אבל זה לא באמת פותר לנו את הבעיה: לא כולם יודעים או זוכרים שכדאי להשתמש ב as כל פעם לפני שמפעילים פונקציה ומהר מאוד נתחיל לראות מתכנתים שמוותרים על זה. דרך קצת יותר ברורה היא להשתמש ב Generics. המילה Generics בסך הכל אומרת שהקוד שקורא ל Hook חייב להעביר גם את סוג המידע שהוא מצפה לקבל, ואז ה Custom Hook שלנו יראה כך:
function useRemoteData<T>(endpoint: string, id: string) {
const [data, setData] = useState<T|null>(null);
useEffect(function() {
setData(null);
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const req = axios.get(`https://swapi.co/api/${endpoint}/${id}/`, {
cancelToken: source.token,
});
req.then(function(response) {
// when we get response
setData(response.data);
});
return function cancel() {
source.cancel();
}
// code continues here
}, [id]);
return data;
}
שני השינויים היחידים הם בשורת ההגדרה של הפונקציה ובשורת הגדרת הטיפוס שנשמר בסטייט. עכשיו הקוד הקודם שמשתמש ב Hook כבר לא מתקמפל כי לא אמרנו מה הטיפוס שאנחנו מצפים לקבל. בשביל להשתמש ב Hook נהיה חייבים להעביר את הטיפוס של תשובת השרת באופן הבא:
function FilmInfo(props: { id: string }) {
const { id } = props;
// Get character data ???
const data = useRemoteData<IDataFilm>('films', id);
if (data === null) {
return <p>Loading, please wait...</p>
}
return (
<div>
<p>title: {data.title}</p>
<p>release_date: {data.release_date}</p>
<hr />
</div>
)
}
המעבר ל Generics יצר אילוץ על כל מי שמשתמש ב Hook כך שעכשיו נהיה חייבים לציין את סוג המידע שאנחנו מצפים לו. גישה כזו מונעת טעויות הקלדה והופכת את תחזוקת הקוד להרבה יותר קלה, שכן כל פעם שנעשה Refactoring ונשנה שמות של שדות או נמחק שדות מתשובת השרת באופן אוטומטי TypeScript יוכל להגיד לנו איזה קומפוננטות הושפעו מהשינוי.