אילוץ גנרי מגביל את מה שפרמטר טיפוס יכול להיות. <T extends { length: number }> מקבל רק טיפוסים שיש להם length מספרי (מחרוזות, מערכים, אובייקטים משלכם), ובזכות ההבטחה הזו מותר לפונקציה לקרוא את .length.
טיפוס ההחזרה הוא עדיין T, לא { length: number }: העברה של שני מערכי מספרים מחזירה number[], והאובייקטים שומרים על ה-tag שלהם. האילוץ מתאר את המינימום; הטיפוס של מי שקורא לפונקציה נשמר.
למה צריך אילוצים
בתוך פונקציה גנרית, T בלי אילוץ יכול להיות כל דבר: מחרוזת, מספר, null, פונקציה. לכן TypeScript מאפשר רק מה שעובד על כל טיפוס, וזה כמעט כלום.
הקומפיילר מדפיס:
index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.
הפתרון הוא לומר מה אתם צריכים: <T extends { length: number }>. אילוצים הופכים "כל טיפוס" ל"כל טיפוס שיש לו את זה", ובדרך כלל זו הכוונה של פונקציה גנרית.
אילוץ ל-interface
האילוץ יכול להיות כל טיפוס, כולל interface או type alias שכבר יש לכם. הפונקציה עובדת אז עם כל דבר שיש לו לפחות את האיברים האלה, ועדיין מחזירה את הטיפוס המלא של מי שקורא לה.
השוו לטיפוס פרמטר רגיל: function byId(items: HasId[], id: number): HasId | undefined מקבלת את אותם מערכים, אבל התוצאה היא רק HasId, ולכן found.name היה שגיאה. הגרסה הגנרית מעבירה הלאה את הטיפוס המלא.
K extends keyof T: גישה בטוחה למאפיינים
האילוץ הנפוץ ביותר מקשר בין שני פרמטרי טיפוס. K extends keyof T אומר ש-K חייב להיות אחד משמות המאפיינים של T, ו-T[K] הוא הטיפוס של המאפיין הזה.
מפתח עם שגיאת כתיב או מפתח שלא קיים הוא שגיאת קומפילציה, וטיפוס ההחזרה נקבע לפי המפתח: "year" נותן number, ו-"title" נותן string. העמוד על keyof מסביר את keyof עצמו, כולל איך הוא מתנהג עם index signatures.
אילוץ לטיפוסים פרימיטיביים
אילוץ יכול להיות טיפוס פרימיטיבי או union שלהם. תופעת לוואי שימושית: כש-T extends string (או number), TypeScript מסיק את טיפוס הליטרל של הארגומנט במקום להרחיב אותו.
בלי האילוץ, createEvent("user.saved") מחזיר { type: string }: כאן TypeScript מרחיב ל-string את הליטרל שהוא הסיק עבור T. עם T extends string הליטרל נשמר, וכך פונקציות עזר עם טיפוסים לאירועים ולניתוב מקבלות שמות מדויקים מארגומנטים רגילים של מחרוזות.
שגיאות נפוצות
החזרה של משהו שרק מתאים לאילוץ. פונקציה שמחזירה T חייבת להחזיר T, ולא סתם ערך שמתאים לאילוץ:
interface HasId {
id: number;
}
function reset<T extends HasId>(item: T): T {
return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
// '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
// instantiated with a different subtype of constraint 'HasId'.
אם T הוא { id: number; name: string }, לאובייקט החדש אין name, ולכן הוא אינו T. החזירו spread של הקלט, return { ...item, id: 0 };, ששומר על כל מאפיין, או הצהירו על טיפוס ההחזרה כ-HasId אם זה כל מה שאתם מייצרים.
העברה של ארגומנט טיפוס ששובר את האילוץ. גם ארגומנטי טיפוס מפורשים נבדקים: עם function scale<T extends number>(x: T), הקריאה scale<string>("2") נכשלת עם Type 'string' does not satisfy the constraint 'number'. (TS2344).
אילוץ כשלא צריך גנרי. אם T מופיע רק בפרמטר, function print<T extends HasId>(item: T): void היא פשוט function print(item: HasId): void עם תחביר מיותר. אילוצים חשובים כש-T מופיע גם בטיפוס ההחזרה או בפרמטר נוסף.
אילוצים עם ערכי ברירת מחדל
לפרמטר טיפוס יכולים להיות גם אילוץ וגם ערך ברירת מחדל. ברירת המחדל חייבת לעמוד באילוץ.
interface Store<TState extends object = Record<string, unknown>> {
get(): TState;
set(next: Partial<TState>): void;
}
type AnyStore = Store; // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>; // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.
אותה מילת מפתח extends מופיעה גם ב-conditional types, T extends string ? A : B, ושם היא בדיקה ולא הגבלה; העמוד על conditional types מסביר את הצורה הזו.
שאלות נפוצות
מה המשמעות של T extends בגנרי של TypeScript?
<T extends Constraint> אומר ש-T יכול להיות כל טיפוס שניתן להשמה ל-Constraint. מי שקורא לפונקציה יכול להעביר רק טיפוסים כאלה, ובתוך הפונקציה מותר להשתמש בכל מה שהאילוץ מבטיח. <T extends { length: number }> מקבל מחרוזות, מערכים וכל אובייקט עם length מספרי, ומאפשר לקרוא את value.length.
מה המשמעות של K extends keyof T?
K חייב להיות אחד משמות המאפיינים של T. יחד עם ה-indexed access type T[K] הוא נותן טיפוס מדויק לגישה למאפיין: function get<T, K extends keyof T>(obj: T, key: K): T[K] מקבל רק מפתחות אמיתיים של obj, וטיפוס ההחזרה הוא הטיפוס של המאפיין הזה.
למה אני מקבל "Property does not exist on type T"?
T בלי אילוץ יכול להיות כל טיפוס, כולל number או null, ולכן TypeScript לא מאפשר עליו שום דבר שספציפי לטיפוס (TS2339). הוסיפו אילוץ שמבטיח את המאפיין: <T extends { name: string }>, או אילוץ ל-interface שכבר יש לכם, <T extends User>.
מה המשמעות של "T could be instantiated with a different subtype of constraint"?
השגיאה TS2322: הפונקציה מבטיחה להחזיר T, אבל מחזירה ערך שמתאים רק לאילוץ. אם T הוא { id: number; name: string }, אובייקט חדש { id: 1 } אינו T. החזירו את הערך שקיבלתם (או spread שלו, עם טיפוס מתאים), או שנו את טיפוס ההחזרה לאילוץ.
מה ההבדל בין extends בגנרי לבין extends במחלקה או ב-interface?
אותה מילת מפתח, רעיון קרוב. ב-class Dog extends Animal וב-interface B extends A היא מצהירה על ירושה. ב-<T extends A> היא מצהירה על אילוץ: T חייב להיות ניתן להשמה ל-A. ב-conditional type, T extends A ? X : Y, היא בדיקה שבוחרת אחד משני טיפוסים.