במאמר הקודם למדנו על פונקציות עם טייפסקריפט – דרך להגדיר לפונקציות את סוג המידע שהן אמורות לקבל ולהחזיר. במאמר הזה אנו נדון בגנריות, מה זה? מדובר בדרך לנהל בפונקציות לא קבועות שהמידע שהן מחזירות (כלומר הפלט) משתנה לפי הקלט. כאשר נתנו למשל דוגמה של פונקציה שעוטפת את fetch שמחזירה פרומיס שהתוצאה שלו מספר או טקסט בהתאם לקריאת ה-API. המשתמש צריך להגדיר, בקריאה לפונקציה, איזה סוג מידע הוא רוצה לקבל.
גנריות
האמת היא שגנריות או תכנות גנרי זה מונח ממדעי המחשב שמגדיר אלגוריתם שמונחה על ידי סוג המידע. אני לא רוצה להכנס לתחום הגנריות אבל נגיד שמתכנתים קלאסיים יותר ירגישו עם הסינטקס הזה יותר בנוח בעוד שמתכנתי ג'אווהסקריפט ירגישו שהוא מוזר. אבל זה חשוב. אני כהרגלי מסביר על הפרקטיקה קודם.
אז בואו ונתחיל בפרקטיקה לא תמיד הפלט של הפוקנציה שלנו הוא אחיד אלא תלוי לא מעט פעמים בקלט שלנו ולא חסרות דוגמאות. למשל פונקציה שמחזירה את האיבר הראשון בכל מערך שהיא מקבלת. בהתחשב בידע שלנו היום, אנו נגדיר אותה כך:
cconst firstElement = (arr: any[]):any => {
return arr[0];
}
firstElement([1,2,3]); // 1
firstElement(['a','b','c']); // 'a'
אנו מגדירים כאן באמצעות arr: any[] שהארגומנט arr יכול להיות כל מערך והפלט של הפונקציה? נהיה חייבים להגדיר אותו כ-any. למה? כי אם האיבר הראשון במערך הוא מחרוזת טקסט, אז יוחזר string, ואם האיבר הראשון הוא מספר, יוחזר מספר. הכל בהתאם למערך. אז מן הסתם אנו לא יכולים לדעת מה עובר ונצטרך להגדיר any. כל דבר.
עם גנריות אפשר להגדיר מה חוזר בהתאם לסוג הקלט. אם אני יודע בוודאות שיוחזר טקסט כשמגיע מערך שהאיברים שלו הם טקסט ויודע בוודאות שיוחזר מספר כשמגיע מערך שהאיברים שלו הם מספרים יש לי דרך לומר את זה לטייפסקריפט. אני מוסיף את מילה כלשהי בתוך סוגריים עם חצים – למשל <Type> לפני הסוגריים של המשתנה כדי "לתפוס" או "להקליט" את מה שהמשתנה מעביר. אני יכול להשתמש במה שהקלטתי כסוג.כלומר אני פשוט מוסיף Type היכן שאני רוצה לבצע את הקשר. הנה הדוגמה שאני מקווה שתבהיר את זה:
const firstElement = <Type>(arr: Type[]): Type => {
return arr[0];
}
firstElement(<number[]>[1,2,3]);
firstElement(<string[]>['a','b','c']);
אז בעצם כשאני משתמש בפונקציה גנרית אני צריך לומר לפונקציה איזה סוג נתונים אני מעביר לה. הפונקציה הגנרית מקבלת את הנתון הזה ויכולה להשתמש בו. למשל, אני מעביר, כשאני קורא לפונקציה שאני שולח מערך עם מספר <number[]> ואז בהגדרה הגנרית יכול להשתמש בה.
בואו נמשיך להסביר את זה, כי אני יודע שזה טיפה מסובך. בואו וניקח פונקציה תלושה מהמציאות שלעולם אין שום צורך בה – פונקציה שמחזירה את כל מה שנותנים לה. איך אני יכול להגדיר בטייפסקריפט מה היא מחזירה? אני לא. כי אם אני מעביר לה מחרוזת טקסט, הפלט יהיה טקסט, אם אני אעביר לה מספר, הפלט יהיה מספר. הפתרון? להקליט את הקלט באמצעות מילה כלשהי (כאן בחרתי ב-Type1) ואז להגדיר שזה יהיה סוג הפלט:
const echoFunction = <Type1>(val:Type1): Type1 => {
return val;
}
למה זה חשוב שאני אדע מה הפלט ולא אשתמש ב-any? כי אם אני אנסה, בשלב מאוחר כלשהו בקוד, להשתמש בתכונה לא מתאימה על התוצאה של הפונקציה, טייפסקריפט יתן לי התראה כי זו שגיאה.
let a = echoFunction<number>(5);
a.length = 5; // Property 'length' does not exist on type 'number'.
למה זה חשוב? אני אתן עוד דוגמה, הפעם מציאותית לגמרי, של גנריות. נניח שיש לי קריאה ל-API. ה-API מחזיר לי promise אבל בתוך ה-promise המידע שיש יכול להיות מחרוזת טקסט או מספר או ווטאבר. במה זה תלוי? רק אני, שקורא ל-API יודע. אז ראשית בפונקציה שמבצעת את הקריאה ל-API אני אבקש מהמשתמש את מה שהוא מצפה לקבל ואגדיר אותו לפי היציאה ובקריאה לפונקציה אני אשתמש בהקלטה הזו כדי לדעת מה אני מקבל.
הסבר מסורבל? בואו נסתכל על הקוד ואז אני אסביר שוב:
async function fetchApi<T>(path: string): Promise<T> {
const response = await fetch(`https://example.com/api${path}`);
return response.json();
}
const stringPromise = fetchApi<string>('callToNumberResultAPI');
stringPromise.then(val => console.log(val.length)); // No problem, string has length
const numberPromise = fetchApi<number>('callToNumberResultAPI');
numberPromise.then(val => console.log(val.length)); // Property 'length' does not exist on type 'number'.
אז מה קורה פה? אני מגדיר את פונקצית fetchAPI ואז מייד אומר לה שמה שהיא מקבלת מהמשתמש (שזה ה-T הראשונה בין החצים) זה מה שהיא מחזירה. אני גם מעביר את זה לסוג Promise שמגיע עם טייפסקריפט.
בקריאה הראשונה אני מגדיר ל-fetchApi שאני מצפה שהתוצאה בתוך ה-Promise תהיה טקסטואלית ואכן לא תהיה שגיאה אם אני אשתמש ב-length על התוצאה. כי זה טקסט!
בקריאה השניה אני מגדיר ל-fetchApi שאני מצפה שהתוצאה בתוך ה-Promise תהיה מספר! ואכן אם אני אשתמש ב-length על התוצאה אני אקבל שגיאה כי אי אפשר להשתמש ב-length על מספר בג'אווהסקריפט.
אני יודע שזה קצת קשה להבנה, אבל בגדול – אני משתמש בגנריות במקרים שאני לא יודע בדיוק איזה סוג מידע אני מקבל מהפונקציה. כאשר סוג המידע תלוי במשתנה או תלוי בקורא. גנריות זו הדרך שלי להגדיר לפונקציה איזה סוג מידע אני אמור לקבל ממנה.
כמובן שניתן להשתמש בכמה פרמטרים ולא רק בפרמטר אחד. למשל, בפונקציה הזו יש לי שני פרמטרים, id ו-name. מה סוג המידע שהפונקציה מחזירה? תלוי בארגומנט הראשון. היא מחזירה מידע מסוג הארגומנט הראשון. מה הוא? רק מי שקורא לפונקציה יכול לדעת ובגלל זה הוא נדרש להכניס את זה (או שלא, ולסמוך על טייפסקריפט שתדע לנחש את הסוג).
function displayType<T, U>(id:T, name:U): T {
// Do something
return id;
}
const result1 = displayType<string, number>('ran', 1);
result1.length;
const result2 = displayType<number, string>(1, 'ran');
result2.length; // Property 'length' does not exist on type 'number'.
גנריות זה נושא שהוא לא תמיד ברור למתכנתי ווב שלא עסקו בכך בעבר אבל הוא מאוד מקובל בשפות אחרות. הוא מאפשר למי שמתשתמש בפונקציות בעצם לקבוע לפונקציות מסוימות בטייפסקריפט איזה סוג מידע הן מחזירות כאשר הפלט שלהן לא קבוע. לפעמים זה בהתאם לקלט, לפעמים זה לפי איך שהמשתמש רוצה.
שימו לב: האותיות T,U,V נחשבות כסטנדרט לסימון קלט של סוג נתון בגנריות אבל אפשר לבחור כל שם שרוצים.
אינטרפייס גנרי
אני יכול להגדיר אינטרפייס גנרי – כלומר אינטרפייס שמחייב פלט של משתנים בהתאם למה שהמשתמש מגדיר. כדי להמחיש זאת, בואו ונגיד שאני רוצה ליצור אינטרפייס של אובייקט שיש לו key ו-value. כלומר כל משתנה שאני מייצר כך הוא עם key ועם value. אדגים עם אינטרפייס פשוט יחסית שאמור להיות מוכר לכם:
interface KeyPair {
key: number;
value: string;
}
זה מעולה אם אנו רוצים שכל מי שישתמש באינטרפייס הזה יקבל אובייקט שה-key שלו הוא מספר וה-value הוא מחרוזת טקסט. למשל משהו כזה:
const var1: KeyPair = { key:1, value: 'ran' };
אבל מה אם אני רוצה שהערך יהיה אובייקט למשל? או מערך? או שה-key יהיה מחרוזת טקסט? זה דברים שקורים הרי. מי יכול לדעת את זה? מי שמשתמש באינטרפייס! אז בדיוק כמו בגנריות רגילה של פונקציה, אנו יכולים לתת אפשרות למשתמש להגדיר מה סוג הפלט שאנו רוצים. איך? בדיוק עם אותו תחביר – באמצעות <> אני מגדיר סוגי נתונים שחייבים להעביר כשמשתמשים באינטרפייס ומכניס אותם לאן שצריך. הנה הדוגמה:
interface KeyPair<T, U> {
key: T;
value: U;
}
const var1: KeyPair<number, string> = { key:1, value: 'ran' };
const var2: KeyPair<number, number> = { key:1, value: 6382020 };
אני יכול לקחת את זה עוד קצת הלאה ולהגדיר מה הפלט או הקלט באמצעות האינטרפייס הגנרי. איך? גם פה – נותן למשתמשים של האינטרפייס לציין את זה. הנה למשל דוגמה נוספת, של אינטרפייס שמחבר בין שני משתנים. מי שמשתמש באינטרפייס יודע מה הוא מצפה לקבל, יודע מה הפלט שלו בהנתן סוג הנתונים שיש והוא זה שיגדיר לאינטרפייס מה הוא רוצה. היתרון הוא שכולם משתמשים באינטרפייס ויש סדר בכל פונקציה. לא יהיה לי מצב שאני מעביר סוג נתון לא טוב או מצפה לקבל סוג נתון שאני לא בונה עליו.
interface CombineTwo<T, U, V>
{
(key: T, val: U): V;
};
const combineStrings:CombineTwo<string,string,string> = (val1, val2) => val1 + val2;
const combineNumbers:CombineTwo<number,number,number> = (val1, val2) => val1 + val2;
const printTwoVarsToConsole:combineTwo<number | string,number | string,void> = (val1, val2) => {
console.log(val1, val2);
};
combineStrings('a','b');
combineNumbers(1, 1);
תראו למשל איך אותו אינטרפייס גנרי עוזר לי בכמה פונקציות דומות ממש שמטפלות בשני סוגי משתנים. פונקציה אחת שמחבר מחרוזות טקסט, פונקציה אחרת שמחברת טקסטים ופונקציה אחרת שמחברת אותם להדפסה בקונסולה ולא מחזירה דבר. הכל מוגדר כמו שצריך על ידי צרכני האינטרפייס וכך צרכני הפונקציות שמשתמשות באותו אינטרפייס יודעים מעולה מה הם מקבלים ומה הם מחזירים. וכן, רואים את זה גם ב-IDE.
במאמר הבא אנו נדבר על קלאסים עם טייפסקריפט. זה לא כזה מורכב כמו גנריות 🙂
2 תגובות
אם אני מאפשר למשתמש בפונקציה לקבוע את סוג הקלט וקובע בהתאם את סוג הפלט, האם אני עדיין יכול גם להגביל את הסוגים החוקיים לקלט? בדוגמאות שנתת נראה שבסופו של דבר T או U יכולים לקבל כל סוג.
לנועם
כן, עם extends. לדוג':
function foo() {}