גנריקס בטייפסקריפט מוסברים¶
גנריקס (generics) הם אחד הנושאים שנשמעים הכי "מפחידים" בטייפסקריפט, בגלל התחביר עם הסוגריים המשולשים - <T>. אבל הרעיון שמאחוריהם פשוט מאוד ברגע שרואים אותו נכון - זו דרך לכתוב קוד שעובד עם כל סוג נתונים, בלי לוותר על הבטיחות שטייפסקריפט נותן.
הבעיה שגנריקס פותרים¶
תארו לעצמכם פונקציה שמחזירה את האיבר הראשון של מערך. אפשר לכתוב אותה כך:
אבל מה אם רוצים את אותה פונקציה גם עבור מערך של מחרוזות? אפשר להשתמש ב-any, אבל זה מוותר לגמרי על בטיחות הטיפוסים - הפונקציה תקבל כל דבר, ותחזיר "כל דבר", בלי שום בדיקה:
function getFirst(arr: any[]): any {
return arr[0];
}
const result = getFirst([1, 2, 3]);
result.toUpperCase(); // אין שגיאה בזמן כתיבה, קריסה בזמן ריצה!
הבעיה ברורה - טייפסקריפט לא יודע יותר מה result בכלל. גנריקס פותרים בדיוק את זה.
הפתרון - גנריק אחד לכל הטיפוסים¶
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const numbers = getFirst([1, 2, 3]); // T הוא number
const words = getFirst(["א", "ב", "ג"]); // T הוא string
T הוא בעצם "משתנה טיפוס" - שם זמני שמייצג "איזה טיפוס שיהיה, שנקבע בכל קריאה לפונקציה". כשקוראים ל-getFirst([1, 2, 3]), טייפסקריפט מבין ש-T הוא number באותה קריאה ספציפית, ולכן יודע שהתוצאה תהיה number. כשקוראים עם מערך מחרוזות, T הופך ל-string. השם T הוא רק מוסכמה - אפשר לקרוא לזה כל שם, אבל T (או T, U, K, V) נפוץ מאוד בעולם הטייפסקריפט.
גנריקס בטיפוסים, לא רק בפונקציות¶
אפשר להשתמש בגנריקס גם כשמגדירים interface או type, כדי ליצור "תבנית" שאפשר למלא עם טיפוסים שונים:
interface ApiResponse<T> {
data: T;
status: number;
error: string | null;
}
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "נועה" },
status: 200,
error: null,
};
const productsResponse: ApiResponse<Product[]> = {
data: [{ id: 1, title: "מוצר" }],
status: 200,
error: null,
};
בלי גנריקס, הייתם צריכים להגדיר ApiResponse נפרד לכל סוג נתונים אפשרי מהשרת. עם גנריקס, ApiResponse<T> הוא תבנית אחת שמשרתת כל תשובת שרת, כשה-T נקבע בכל שימוש.
הגבלת גנריקס עם extends¶
לפעמים רוצים לוודא שהטיפוס הגנרי עומד בתנאי מסוים, למשל שיש לו שדה id. עושים את זה עם extends בהגדרת הגנריק:
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
עכשיו findById יכול לעבוד עם כל מערך, כל עוד לאיברים שלו יש שדה id מסוג number. אם תנסו להשתמש בזה על מערך בלי שדה id, טייפסקריפט יזהיר אתכם מיד.
דוגמה מוכרת - useState בריאקט¶
אם אי פעם הקלדתם useState<string>("") בפרויקט טייפסקריפט, השתמשתם בגנריק בלי לשים לב. ההגדרה של useState בספריית הטיפוסים של ריאקט משתמשת בגנריק כדי לדעת איזה טיפוס יש למה שהוא מנהל:
const [name, setName] = useState<string>("");
// name הוא string, setName מקבל רק string
const [user, setUser] = useState<User | null>(null);
// user יכול להיות User או null
בלי הגנריק, טייפסקריפט היה מנחש את הטיפוס לפי הערך ההתחלתי בלבד, מה שלא תמיד מדויק מספיק - למשל, useState(null) בלי גנריק היה נותן טיפוס null בלבד, שלא מאפשר לעדכן את הסטייט לשום ערך אחר בעתיד.
מתי כדאי להשתמש בגנריקס בעצמכם¶
רוב הזמן, כמפתחי פרונטאנד, אתם צורכים גנריקס יותר משאתם כותבים אותם - useState<T>, Array<T>, Promise<T> כולם גנריקס שספריות הגדירו בשבילכם. לכתוב גנריקס בעצמכם שווה בעיקר כשכותבים פונקציות עזר כלליות שאמורות לעבוד עם כמה סוגי נתונים שונים, כמו פונקציית מיון, פונקציית חיפוש, או הוק מותאם אישית.
בקורס צד לקוח גנריקס מוסברים אחרי היסודות של טייפסקריפט, עם דוגמאות שמראות בדיוק איפה הם מופיעים בקוד ריאקט אמיתי שאתם כבר כותבים.
לסיכום¶
גנריקס נותנים לפונקציות וטיפוסים לעבוד עם כל סוג נתונים, בלי לוותר על בטיחות הטיפוסים כמו ש-any עושה. T הוא רק שם זמני לטיפוס שנקבע בכל שימוש. גם אם לא כותבים אותם בעצמכם ביומיום, להבין אותם עוזר להבין את רוב ספריית הטיפוסים של ריאקט וספריות אחרות.
מתלבטים איך לכתוב פונקציה גנרית לבעיה שלכם? תשאלו בדיסקורד.