مقدمة إلى Generics في TypeScript
TypeScript هي لغة برمجة شديدة التحديد (strictly typed) تضيف كتابة ثابتة (static typing) إلى JavaScript. واحدة من أقوى الإمكانيات وأكثرها استخفافًا في TypeScript هي Generics (الأجناس، الأنواع العامة). إذا كنت قد كتبت يومًا دالة يجب أن تعمل مع أنواع بيانات مختلفة، أو أنشأت مكونًا يجب أن يكون عالميًا، فإن Generics هي بالضبط ما تحتاجه.
في هذه المقالة، سنشرح بالتفصيل ما هي Generics في TypeScript، ولماذا هي ضرورية وكيفية استخدامها عمليًا. ستتعلم كيف تجعل كودك أكثر مرونة وأمانًا وقابلية لإعادة الاستخدام.
ما هي Generics ولماذا هي ضرورية؟
Generics (الأنواع العامة) هي آلية تسمح بإنشاء مكونات (دوال، كلاسات، واجهات) تعمل مع أنواع بيانات مختلفة دون فقدان معلومات النوع. تخيل أنك تكتب دالة تُرجع العنصر الأول من مصفوفة. بدون Generics، سيتعين عليك استخدام النوع any، مما يحرمك من مزايا التحديد الصارم (strict typing).
لماذا نستخدم Generics؟
- أمان الأنواع: يتحقق TypeScript من الأنواع في مرحلة التجميع (compilation)، مما يمنع الأخطاء.
- إعادة استخدام الكود: يمكن لدالة أو كلاس واحد العمل مع أي أنواع بيانات.
- تحسين قابلية القراءة: يصبح الكود أكثر وضوحًا ويوثق نفسه بنفسه.
- تقليل التكرار: لست بحاجة لكتابة دوال منفصلة لكل نوع بيانات.
بدون Generics، كنت ستكتب شيئًا كهذا:
// سيء: نفقد معلومات النوعfunction getFirstElement(arr: any[]): any { return arr[0];}
const num = getFirstElement([1, 2, 3]); // النوع anyconsole.log(num.toUpperCase()); // خطأ أثناء التنفيذ!مع Generics، يصبح الكود آمنًا ومرنًا:
// جيد: نحافظ على النوعfunction getFirstElement<T>(arr: T[]): T | undefined { return arr[0];}
const num = getFirstElement([1, 2, 3]); // النوع numberconst str = getFirstElement(['a', 'b', 'c']); // النوع string
// num.toUpperCase(); // خطأ في التجميع! number ليس لديه طريقة toUpperCaseأساسيات بناء جملة Generics
بناء الجملة باستخدام الأقواس الزاوية
تستخدم Generics الأقواس الزاوية <> لتحديد معاملات النوع (type parameters). عادةً ما تُرمز المعاملات بحرف واحد: T (Type)، U، V، إلخ. ومع ذلك، يمكنك استخدام أي أسماء ذات معنى.
// مثال بسيط: دالة بمعامل نوع واحدfunction identity<T>(arg: T): T { return arg;}
// تحديد النوع بشكل صريحlet result1 = identity<string>('Hello');
// استنتاج النوع (TypeScript يحدد النوع بنفسه)let result2 = identity(42); // النوع numberمعاملات نوع متعددة
يمكنك استخدام عدة معاملات نوع في دالة أو كلاس واحد:
function merge<T, U>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 };}
const merged = merge({ name: 'John' }, { age: 30 });// النوع: { name: string } & { age: number }console.log(merged.name); // Johnconsole.log(merged.age); // 30القيود (Constraints)
أحيانًا تحتاج إلى تقييد الأنواع التي يمكن تمريرها إلى Generic. لهذا، تُستخدم الكلمة المفتاحية extends:
interface HasLength { length: number;}
// الدالة تعمل فقط مع الأنواع التي لديها خاصية lengthfunction logLength<T extends HasLength>(arg: T): T { console.log(arg.length); return arg;}
logLength('Hello'); // يعمل: 5logLength([1, 2, 3]); // يعمل: 3// logLength(123); // خطأ! number ليس لديه length