Generics
לפעמים פונקציה עושה אותו דבר על כל סוג של נתון: מחזירה את האיבר הראשון במערך, עוטפת תשובה של API, שומרת ערך במטמון. Generics מאפשרים לכתוב אותה פעם אחת, בלי לאבד את הטיפוס.
הבעיה
// עם any: עובד, אבל הטיפוס הולך לאיבוד
function first(items: any[]): any {
return items[0];
}
const name = first(['דנה', 'יוסי']); // any - אין השלמה, אין בדיקה
הפתרון: פרמטר של טיפוס
function first<T>(items: T[]): T | undefined {
return items[0];
}
const name = first(['דנה', 'יוסי']); // string | undefined
const num = first([1, 2, 3]); // number | undefined
T הוא "משתנה של טיפוס": כשקוראים לפונקציה, TypeScript מציבה בו את הטיפוס האמיתי. בדרך כלל היא מסיקה אותו לבד מהארגומנטים, ולא צריך לכתוב first<string>(...).
כבר השתמשתם ב-Generics
const ids: Array<number> = [1, 2];
const user: Promise<User> = fetchUser(1);
const cache = new Map<string, number>();
const [count, setCount] = useState<number>(0); // React
טיפוס גנרי
// תשובה אחידה מה-API, עם כל סוג של נתונים
type ApiResponse<T> =
| { ok: true; data: T }
| { ok: false; error: string };
async function getJson<T>(url: string): Promise<ApiResponse<T>> {
try {
const response = await fetch(url);
if (!response.ok) return { ok: false, error: `סטטוס ${response.status}` };
return { ok: true, data: (await response.json()) as T };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : 'שגיאת רשת' };
}
}
const result = await getJson<Product[]>('/api/products');
if (result.ok) {
console.log(result.data.length); // Product[]
} else {
console.log(result.error);
}
כאן חובה לכתוב <Product[]>, כי אין לטיפוס ממה להיות מוסק.
הגבלות: extends
לפעמים הפונקציה צריכה משהו מינימלי מהטיפוס. extends דורש אותו:
// T יכול להיות כל דבר, בתנאי שיש לו id
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
const product = findById(products, 3); // Product | undefined, עם כל השדות
findById(['a', 'b'], 1);
// Type 'string' does not satisfy the constraint '{ id: number; }'.
keyof: שמות השדות כטיפוס
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const names = pluck(products, 'name'); // string[]
const prices = pluck(products, 'price'); // number[]
pluck(products, 'colour');
// Argument of type '"colour"' is not assignable to parameter of type 'keyof Product'.
מתי להשתמש
Generic מתאים כשהטיפוס של הקלט קובע את הטיפוס של הפלט. אם T מופיע רק פעם אחת בחתימה, כנראה שהוא מיותר, ו-unknown או טיפוס רגיל יספיקו.
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה היתרון של
function first<T>(items: T[]): T | undefinedעל פני any?הצגת התשובה
תשובה ג. first(['a']) מחזיר string | undefined ולא any.
-
מה עושה
T extends { id: number }?הצגת התשובה
תשובה ב. כך הפונקציה יכולה להשתמש ב-item.id בבטחה.