لو عندك دالة بتاخد array وترجّع أول عنصر فيه، وعامل منها نسخة لـ string[] وتانية لـ number[] وتالتة لـ User[]، أنت بتعيد نفس اللوجيك 3 مرات. Generics في TypeScript بتخليك تكتبها مرة واحدة مع الحفاظ على الـ type بالظبط لكل نداء.
TypeScript Generics من غير لف
المشكلة باختصار
فيه طريقتين شائعتين للتعامل مع دوال بتشتغل على أكتر من نوع، وكلاهما بيجيب مشاكل في production:
- تكرار الدالة لكل نوع: كود ميت وصيانة مكلّفة.
- استخدام
any: بتفقد كل فوائد TypeScript وبترجع لحالة JavaScript النقية.
Generic param هو ببساطة نوع متغيّر بيتحدّد وقت النداء. نفس الفكرة زي بارامتر عادي في دالة، بس بتمرّر فيه نوع بدل قيمة.
أول مثال: دالة identity
الصيغة الأساسية: حرف واحد (العُرف T) بين <> بعد اسم الدالة.
function identity<T>(value: T): T {
return value;
}
const a = identity(42); // a: number
const b = identity("hello"); // b: string
const c = identity([1, 2, 3]); // c: number[]
ركّز على حاجة مهمة: TypeScript استنتج T تلقائيًا من الـ argument. مش لازم تكتب identity<number>(42) إلا لو الاستنتاج فشل أو مش واضح.
مثال أقرب للشغل اليومي: getFirst
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}
interface User { id: string; name: string; }
const users: User[] = [{ id: "1", name: "Ahmed" }];
const first = getFirst(users); // first: User | undefined
console.log(first?.name); // TypeScript عارف إن فيه name
قارنها بالإصدار بـ any:
function getFirstBad(items: any[]): any {
return items[0];
}
const first = getFirstBad(users);
first.nammmme; // لا خطأ compile، bug في runtime
الفرق مش تجميلي. الإصدار الأول بيمسك الـ typo وقت الكتابة، التاني بيكسر في الـ browser عند المستخدم.
Constraints: لما تحتاج تضمن إن النوع فيه خاصية معيّنة
أحيانًا الدالة محتاجة تشتغل على أي نوع، بس بشرط إن النوع ده يحتوي على خاصية. استخدم extends.
interface HasId { id: string; }
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
findById(users, "1"); // شغّال لأن User فيه id
findById([{ name: "x" }], "1"); // خطأ compile: مفيش id
الفايدة: الدالة مرنة (بتشتغل على User، Product، Order... أي نوع فيه id)، مع الحفاظ على الـ return type الحقيقي. مش مجرد HasId، ده T الكامل بكل خصائصه.
أكتر من Generic في نفس الدالة
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) result[key] = obj[key];
return result;
}
const user = { id: "1", name: "Ahmed", email: "a@b.com" };
const slim = pick(user, ["id", "name"]);
// slim: { id: string; name: string }
// slim.email // خطأ compile: مش موجود
دي دالة pick كاملة بـ 4 سطور، type-safe تمامًا. لو جربت تعمل نفس الحاجة بـ any أو بدون generics هتلاقي نفسك بتفقد المعلومة عن الـ keys المختارة.
سيناريو واقعي: client بيضرب REST API
async function apiGet<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
interface Product { id: string; price: number; }
const product = await apiGet<Product>("/api/products/1");
console.log(product.price); // TypeScript يعرف النوع
هنا Generic بتخلّي دالة واحدة تخدم كل الـ endpoints. بس لاحظ الـ trade-off: الـ as Promise<T> كذبة للـ compiler. في runtime مفيش ضمان إن الـ API رجّع فعلًا الشكل ده. لو عندك حالات حرجة استخدم zod أو io-ts للـ validation الفعلي.
trade-offs لازم تعرفها
- المكسب: إعادة استخدام بدون فقدان الـ type، اكتشاف أخطاء وقت الكتابة، IntelliSense بيظهر الخصائص الحقيقية بعد النداء.
- التكلفة: الصياغة بتبقى أصعب للقراءة لما تتداخل (
<T, K extends keyof T, V extends T[K]>). فريق مبتدئ في TS بيتعثر فيها. - الافتراض: الشرح ده مبني على TypeScript 4.7+. فيه تحسينات على الاستنتاج في الإصدارات الأحدث بتقلّل عدد المرات اللي بتحتاج فيها تكتب الـ generic يدويًا.
متى لا تستخدم Generics
Generic مش هدف في حد ذاته. لا تستخدمه في الحالات دي:
- الدالة شغّالة على نوع واحد بس ومفيش نية لتوسيع. اكتب النوع صراحةً أوضح.
- الـ generic param ظهر مرة واحدة في الـ signature ومش راجع في الـ output. غالبًا ده علامة code smell: النوع ملوش لازمة.
- بتكتب مكتبة utility داخلية صغيرة فيها 10 دوال generic متداخلة. ممكن تبسّطها بـ union types بدل ما تستعرض قدرات الـ type system.
قياس تقريبي من مشاريع متوسطة: لو أكتر من 40% من دوالك generic، ده مؤشر إنك بتعمل abstraction مبكّرة.
الخطوة التالية
افتح أقرب ملف .ts فيه any عندك وشوف أول 3 حالات. لو أي حالة منهم في signature دالة ومش boundary خارجي (زي JSON input غير موثوق)، حوّلها لـ generic. لو الـ compile فضل شغّال والـ tests عدّت، كسبت type safety ببلاش. لو وقعت، المكان ده كان بالظبط فيه bug محتمل مختبّي.