قيد الـ generic يحدد ما يمكن أن يكونه معامل النوع. لا يقبل <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 نفسه، بما في ذلك سلوكه مع توقيعات الفهرسة.
التقييد بالأنواع البدائية
يمكن أن يكون القيد نوعًا بدائيًا أو union منها. ومن الآثار الجانبية المفيدة: حين يكون T extends string (أو number) تستنتج TypeScript النوع الحرفي للوسيط بدل توسيعه.
دون القيد يعيد createEvent("user.saved") النوع { type: string }: هنا توسّع TypeScript القيمة الحرفية التي استنتجتها لـ T إلى string. ومع 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. أعد نسخة من المدخل بالنشر، return { ...item, id: 0 };، التي تحتفظ بكل الخصائص، أو صرّح بأن نوع القيمة المُعادة HasId إن كان هذا كل ما تنتجه.
تمرير وسيط نوع يخالف القيد. وسائط النوع الصريحة تُفحص أيضًا: مع function scale<T extends number>(x: T) يفشل الاستدعاء scale<string>("2") بالرسالة Type 'string' does not satisfy the constraint 'number'. (TS2344).
التقييد حين لا تحتاج إلى generic. إن ظهر 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 نفسها أيضًا في الأنواع الشرطية، T extends string ? A : B، حيث تكون اختبارًا لا تقييدًا؛ وتشرح صفحة الأنواع الشرطية هذه الصيغة.
الأسئلة الشائعة
ماذا تعني T extends في generic بـ TypeScript؟
تعني <T extends Constraint> أن T يمكن أن يكون أي نوع قابل للإسناد إلى Constraint. لا يستطيع المستدعون تمرير غير هذه الأنواع، وداخل الدالة يمكنك استخدام كل ما يضمنه القيد. يقبل <T extends { length: number }> النصوص والمصفوفات وأي كائن له length رقمية، ويسمح لك بقراءة value.length.
ماذا تعني K extends keyof T؟
يجب أن يكون K أحد أسماء خصائص T. ومع نوع الوصول المفهرس 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. أعد القيمة التي استقبلتها (أو نسخة منها بالنشر، بنوع مناسب)، أو غيّر نوع القيمة المُعادة إلى القيد.
ما الفرق بين extends في generic وextends في صنف أو interface؟
الكلمة نفسها والفكرة متقاربة. في class Dog extends Animal وinterface B extends A تعلن الوراثة. وفي <T extends A> تعلن قيدًا: يجب أن يكون T قابلًا للإسناد إلى A. وفي النوع الشرطي T extends A ? X : Y هي اختبار يختار أحد نوعين.