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 או טיפוס רגיל יספיקו.

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה היתרון של function first<T>(items: T[]): T | undefined על פני any?

    1. אין
    2. הוא מהיר יותר
    3. הטיפוס של הקלט נשמר גם בפלט
    4. הוא עובד רק על מספרים
    הצגת התשובה

    תשובה ג. first(['a']) מחזיר string | undefined ולא any.

  2. מה עושה T extends { id: number }?

    1. הופך את T ל-number
    2. מגביל את T לטיפוסים שיש להם לפחות id מספרי
    3. יוצר id
    4. יורש מחלקה
    הצגת התשובה

    תשובה ב. כך הפונקציה יכולה להשתמש ב-item.id בבטחה.