Generics הם פרמטרי טיפוס. פונקציה גנרית מצהירה על טיפוס שומר מקום, שבדרך כלל נקרא T, וכל קריאה ממלאת אותו, כך שהטיפוסים שנכנסים קובעים את הטיפוסים שיוצאים. פונקציה אחת עובדת אז עם מספרים, מחרוזות ואובייקטים משלכם, וכל קריאה עדיין מקבלת טיפוסים מלאים.
<T> אחרי שם הפונקציה מצהיר על פרמטר הטיפוס. items: T[] משתמש בו לקלט ו-T | undefined לפלט, וזה קושר את השניים. אף פעם לא כתבתם number או string: TypeScript הסיק את T מהארגומנט.
למה לא any או unknown?
בלי generics הייתם נותנים לפרמטר את הטיפוס any או unknown. שניהם מקבלים כל מערך, ושניהם מאבדים את הקשר בין הקלט לפלט:
| טיפוס הפרמטר | מקבל כל קלט | טיפוס התוצאה | נבדק |
|---|---|---|---|
any | כן | any | לא: טעויות מתקמפלות |
unknown | כן | unknown | כן, אבל צריך לצמצם לפני השימוש |
T (גנרי) | כן | קשור לקלט | כן, עם הטיפוס המדויק |
הסקת ארגומנטי טיפוס
בדרך כלל TypeScript מסיק את ארגומנטי הטיפוס מהערכים שמעבירים. אפשר גם לכתוב אותם במפורש בסוגריים משולשים בקריאה, וזה נחוץ כששום דבר בארגומנטים לא מזכיר את T, או כשההסקה בוחרת משהו שלא רציתם.
כדאי להכיר שתי תוצאות הסקה. pair(1, "one") בלי הארגומנט המפורש היא שגיאה: TypeScript מסיק T = number מהארגומנט הראשון ואז מדווח Argument of type 'string' is not assignable to parameter of type 'number'. (TS2345). ו-emptyList() בלי ארגומנט בכלל מקבלת T = unknown, כלומר unknown[], וזה רק לעתים רחוקות מה שרוצים.
כמה פרמטרי טיפוס
פונקציה יכולה להצהיר על כמה פרמטרי טיפוס שהיא צריכה, מופרדים בפסיקים. כל אחד מהם מוסק בנפרד.
mapValues מסיקה שלושה דברים בבת אחת: K הוא "tea" | "cake", V הוא number, ו-R הוא string מתוך ערך ההחזרה של ה-callback. K extends string הוא אילוץ, והוא מוסבר בעמוד הבא.
interfaces ו-type aliases גנריים
גם טיפוסים יכולים לקבל פרמטרים. אז כותבים את הארגומנט כשמשתמשים בטיפוס: Box<number>, ApiResponse<User>. רוב טיפוסי האוספים המובנים עובדים כך: Array<T>, Map<K, V>, Promise<T>, Record<K, V>.
Result<T, E = string> מדגים גם פרמטר טיפוס עם ברירת מחדל: Result<number> פירושו Result<number, string>. כמו פרמטרים אופציונליים של פונקציה, פרמטרים עם ברירת מחדל חייבים לבוא אחרי הפרמטרים החובה.
מחלקות גנריות
מחלקה מקבלת פרמטרי טיפוס אחרי השם שלה, וכל מופע קובע אותם. שדות, מתודות ופרמטרים של הבנאי יכולים כולם להשתמש בהם.
אם הבנאי מקבל T, אפשר לוותר על הארגומנט המפורש: new Box(5) מסיק Box<number>. איברים סטטיים שייכים למחלקה עצמה ולא למופע, ולכן הם לא יכולים להשתמש בפרמטר הטיפוס של המחלקה (Static members cannot reference class type parameters., TS2302).
פונקציות חץ וטיפוסי פונקציות גנריים
רשימת פרמטרי הטיפוס באה לפני רשימת הפרמטרים. אותו תחביר מתאר טיפוס של פונקציה גנרית.
const last = <T>(items: T[]): T | undefined => items[items.length - 1];
type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);
// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];
הסקת טיפוסי ליטרל עם פרמטרי טיפוס const
כברירת מחדל גנרי מסיק טיפוסים מורחבים: pair("a", "b") נותן [string, string]. כשהערכים המדויקים חשובים (שמות נתיבים, שמות אירועים, רשימות עמודות), סמנו את הפרמטר כ-const (מ-TypeScript 5.0 ואילך), והוא יסיק כאילו הארגומנט נכתב עם as const:
מי שקורא לפונקציה כותב מערך רגיל, והפונקציה שומרת על טיפוסי הליטרל.
מתי לא להשתמש בגנריקס
פרמטר טיפוס מצדיק את מקומו כשהוא מקשר בין שני דברים: פרמטר וטיפוס ההחזרה, שני פרמטרים, או פרמטר ו-callback. אם T מופיע רק פעם אחת, הוא לא מוסיף כלום וטיפוס רגיל ברור יותר.
// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
console.log(value);
}
// Clearer
function logValueSimple(value: unknown): void {
console.log(value);
}
// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
return JSON.parse(text);
}
המקרה האחרון נפוץ ומטעה: parseJson<User>(text) נראה בטוח מבחינת טיפוסים אבל לא בודק כלום, בדיוק כמו JSON.parse(text) as User. החזירו unknown ובצעו ולידציה במקום זה. סימנים נוספים לשימוש מופרז: פרמטר טיפוס שתמיד מקבל את אותו ארגומנט, או כזה שאפשר להחליף ב-union של שני טיפוסים ידועים.
לגנריקס שצריכים לדעת משהו על T (שיש לו length, או ש-K הוא מפתח של T), אילוצים עם extends הם השלב הבא.
שאלות נפוצות
מה זה generics ב-TypeScript?
Generics הם פרמטרי טיפוס: שומרי מקום כמו T שמתמלאים בטיפוס אמיתי בכל פעם שמשתמשים בפונקציה, ב-interface, ב-type alias או במחלקה. function first<T>(items: T[]): T | undefined עובדת עם כל מערך, והתוצאה מקבלת את טיפוס האיברים של המערך שהעברתם, כך ש-first([1, 2]) הוא number | undefined ו-first(["a"]) הוא string | undefined.
מה ההבדל בין generics ל-any?
any מכבה את בדיקת הטיפוסים: אין קשר בין הערך שנכנס לערך שיוצא, וגם התוצאה היא any. גנרי שומר על הקשר: identity<T>(x: T): T מחזירה בדיוק את הטיפוס שהעברתם. השתמשו ב-unknown אם אתם מקבלים כל דבר אבל לא צריכים להחזיר את הטיפוס, ובגנרי אם כן.
מה המשמעות של <T> ב-TypeScript?
זו הצהרה על פרמטר טיפוס בשם T. ב-function wrap<T>(value: T), T הוא משתנה טיפוס ש-TypeScript ממלא מתוך הארגומנט בכל קריאה, או שאתם מעבירים במפורש כמו wrap<string>("a"). השם T הוא רק מוסכמה; שמות ארוכים יותר כמו TItem או Key עובדים באותה צורה.
איך כותבים פונקציית חץ גנרית ב-TypeScript?
שמים את רשימת פרמטרי הטיפוס לפני הפרמטרים: const first = <T>(items: T[]): T | undefined => items[0];. בקובץ .tsx, <T> ייקרא כתגית JSX, אז כתבו <T,> עם פסיק בסוף, או <T extends unknown>.
איך מגדירים טיפוס ברירת מחדל לגנרי ב-TypeScript?
מוסיפים = Type אחרי הפרמטר: interface ApiResponse<T = unknown> { data: T }. אז ApiResponse בלי ארגומנט פירושו ApiResponse<unknown>. פרמטרים עם ערכי ברירת מחדל חייבים לבוא אחרי אלה שבלי, כמו פרמטרים אופציונליים של פונקציה.