كيف تُمكنك TypeScript Generics من كتابة كود أقل وأكثر كفاءة
عندما يتناقش المطورون حول لغة TypeScript، غالبًا ما يكون أحد اعتراضاتهم الرئيسية هو الحاجة إلى كتابة المزيد من الكود لتحقيق نفس النتائج مقارنةً بلغات أخرى. ومع أن هذا الرأي يحمل بعض الصحة، إلا أنني أرى أن ميزة Generics في TypeScript تمتلك القدرة على تقليل كمية الكود الذي تحتاج لكتابته بشكل كبير، مما يعزز الكفاءة والإنتاجية.
ما هي TypeScript Generics؟
تُعد Generics في TypeScript أداة قوية لإضافة مستوى من التجريد إلى الواجهات (interfaces) الخاصة بك. باستخدام Generics، يمكنك تمرير واجهة كمعامل (param) إلى واجهة أخرى، مما يتيح لك إنشاء مكونات قابلة لإعادة الاستخدام ومرنة. لنلقِ نظرة على مثال لنموذج استجابة API قياسي، والذي يغطي كلاً من سيناريو النجاح وحالة الخطأ.
// successful response ✅
{ status : 'ok' , responseCode : 200 , data : {...} }
// error response ❌
{ responseCode : 500 , errorMessage : "Something went wrong ?" ; }
بدلاً من كتابة واجهة منفصلة لكل نوع استجابة وتكرار هذه المفاتيح، يمكنك ببساطة الاستفادة من Generics لإنشاء هيكل أكثر مرونة وشمولية كما يلي:
interface ApiResponse<T>{
errorMessage?: string ;
responseCode?: string ;
data?: T;
}
interface UserData {
name: string ;
email: string ;
}
const response: ApiResponse<UserData> = {}
ربط Generics بالدوال (Functions)
لنفترض أن لدينا دالة نستخدمها لإرسال طلبات API إلى الواجهة الخلفية (backend) لتطبيقنا.
const getRequestTo = ( endpoint: string ) => {
return fetch(process.env.BE_HOST + endpoint).then( res => res.json())
}
const userResponse = getRequestTo( '/user-data' )
في هذا السيناريو، سيكون نوع المتغير userResponse هو any، مما يُفقدنا ميزة التحقق من الأنواع في TypeScript. يمكننا تحسين هذا التنفيذ باستخدام Generics للحصول على تجربة أفضل مع TypeScript.
const getRequestTo = async <R>(endpoint: string ): Promise <ApiResponse<R>> => {
const request = await fetch(process.env.BE_HOST + endpoint);
const response = await request.json();
return response;
};
يمكننا إنشاء دالة مشابهة لطلبات POST، والتي تستقبل بيانات JSON كمعاملات من النوع B، ويتوقع أن يُعيد الخادم استجابة JSON من النوع R:
const postRequestTo = async <B, R>(
endpoint: string ,
body: B
): Promise <ApiResponse<R>> => {
const request = await fetch(process.env.BE_HOST + endpoint, {
method: "POST" ,
body: JSON .stringify(body),
});
const response = await request.json();
return response;
};
وبالمثل، يمكننا بناء دالة لطلبات PATCH، والتي تُستخدم للتعامل مع التحديثات الجزئية لأي كيان (entity).
const patchRequestTo = async <B, R>(endpoint: string ,body: Partial<B>): Promise <ApiResponse<R>> => {
const request = await fetch(process.env.BE_HOST + endpoint, {
method: "PATCH" ,
body: JSON .stringify(body)
});
const response = await request.json();
return response;
};
إليك كيفية تطبيق هذا النمط من الدوال:
interface RequestBody {}
interface Response {}
const createPost = await postRequestTo<RequestBody, Response>( '/post' , postData);
const updatePost = await patchRequestTo<RequestBody, Response>( '/post' , { title: "new name" });
تجميع كل شيء في فئة JavaScript بسيطة
دعنا نجمع كل هذه المفاهيم معًا في فئة JavaScript بسيطة لإنشاء خدمة API متكاملة:
class ApiService<T> {
constructor ( entitySlug: string ) {
this .entitySlug = entitySlug;
}
private entitySlug: string ;
getOne = async (id: string ): Promise <ApiResponse<T>> => {
const request = await fetch(process.env.BE_HOST + this .entitySlug + '/' + id);
const response = await request.json();
return response;
};
getList = async (): Promise <ApiResponse<T[]>> => {
const request = await fetch(process.env.BE_HOST + this .entitySlug);
const response = await request.json();
return response;
};
create = async (body: Omit<T, 'id' | 'created' | 'updated' >): Promise <ApiResponse<T>> => {
const request = await fetch(process.env.BE_HOST + this .entitySlug, {
method: 'POST' ,
body: JSON .stringify(body)
});
const response = await request.json();
return response;
};
update = async (
body: Omit<Partial<T>, 'id' | 'created' | 'updated' >
): Promise <ApiResponse<T>> => {
const request = await fetch(process.env.BE_HOST + this .entitySlug + '/' + body.id, {
method: 'PATCH' ,
body: JSON .stringify(body)
});
const response = await request.json();
return response;
};
delete = async (id: string ): Promise < void > => {
const request = await fetch(process.env.BE_HOST + this .entitySlug + '/' + id, {
method: 'DELETE'
});
const response = await request.json();
return response;
};
};
وبعد ذلك، يمكنك إنشاء خدمة كيان (entity service) محددة بسهولة تامة على النحو التالي:
export const postService = new ApiService<Post>( '/post' );
وبهذا تكون جميع المكونات مرتبطة وجاهزة للاستخدام الفوري.

تُظهر الصورة أعلاه كيف يقوم محرر VS Code بتقديم اقتراحات تلقائية (auto-suggesting) ذكية ودقيقة، وذلك بفضل التكوين المتقن لـ TypeScript الذي قمنا بتطبيقه باستخدام Generics.
تمتلك TypeScript إمكانات هائلة لتعزيز تجربة المطورين بشكل كبير، خاصةً إذا تم تكوينها واستخدامها بفعالية. ما عرضناه هنا هو بعض الاستراتيجيات الأساسية التي تجعل عملية التكوين هذه أكثر سلاسة وراحة. نأمل أن يساعدك هذا الشرح في الاستفادة القصوى من TypeScript Generics في مشاريعك الحالية والمستقبلية، وتحسين جودة الكود وكفاءته.
الخلاصة التقنية
تُبرهن TypeScript Generics على أنها ليست مجرد ميزة إضافية، بل هي حجر الزاوية في بناء تطبيقات JavaScript قابلة للتوسع والصيانة. من خلال تمكين المطورين من كتابة كود مرن وقابل لإعادة الاستخدام، تقلل Generics من الحاجة إلى تكرار الكود (boilerplate code) وتُعزز من سلامة الأنواع (type safety). هذا يؤدي إلى تقليل الأخطاء في وقت التشغيل (runtime errors) وتحسين تجربة التطوير بشكل عام، مما يجعلها أداة لا غنى عنها لأي مشروع TypeScript جاد يهدف إلى الكفاءة والإنتاجية العالية.