اكتشف كيف تحول Generic وConditional Types في TypeScript من أدوات بسيطة إلى أسلحة سرية للمطورين المحترفين، وكيف تستخدمها لكتابة كود أنظف وأسرع وأكثر أماناً دون تعقيدات غير ضرورية.
تخيل أنك تعمل على مكتبة مكونات واجهة مستخدم ضخمة مثل Material-UI أو Ant Design، وتريد إنشاء مكون Button مرن يقبل أنواعاً مختلفة من Props بناءً على السياق: زر نصي، زر أيقونة، زر تحميل، أو زر مخصص بالكامل. المشكلة؟ إذا استخدمت union types بسيطة مثل `Butt TextProps | IconProps | LoadingProps`، ستضطر للتعامل مع نوع غير محدد داخل المكون، مما يفقدك فوائد TypeScript القوية. هنا يأتي دور Generic وConditional Types ليس فقط لحل المشكلة، بل لإعادة تعريف طريقة تفكيرك في تصميم الأنواع نفسها.
الحقيقة هي أن معظم المطورين يستخدمون Generic Types كوسيلة لإعادة الاستخدام فقط، مثل `function identity<T>(arg: T): T`، لكنهم يتوقفون عند هذا الحد. لكن المحترفون يعرفون أن Generic ليست مجرد أداة لتقليل التكرار، بل هي لغة برمجة مصغرة داخل TypeScript تسمح لك ببناء أنظمة أنواع ديناميكية تتفاعل مع بعضها البعض. وعندما تضيف Conditional Types إلى المعادلة، تصبح قادراً على كتابة منطق أنواع معقد ينفذ في وقت الترجمة، مما يقلل الأخطاء في وقت التشغيل ويحسن أداء التطبيق بشكل ملموس.
عندما نتحدث عن Generic في TypeScript، فإننا لا نتحدث عن مجرد تمرير نوع كمعامل. بل نتحدث عن بناء دوال وأنواع تتعامل مع الأنواع كقيم يمكن معالجتها. فكر في Generic كدالة تأخذ نوعاً وتعيد نوعاً آخر، تماماً كما تأخذ الدالة العادية قيمة وتعيد قيمة. الفرق الوحيد هو أن هذه العملية تحدث في وقت الترجمة، وليس وقت التشغيل، مما يعني أنها بلا تكلفة أداء على الإطلاق.
لنأخذ مثالاً عملياً من مكتبة Redux Toolkit. عند تعريف `createSlice`، تستخدم Generic لتحديد نوع الـ State و الـ Action و الـ Reducer. لكن المحترفون لا يتوقفون عند هذا الحد، بل يستخدمون Generic لبناء أنواع متداخلة تتفاعل مع بعضها. مثلاً، يمكنك تعريف نوع `PayloadAction<P, T>` حيث `P` هو نوع الـ Payload و`T` هو نوع الـ Type، ثم استخدامه لبناء نظام أنواع كامل للـ Actions دون تكرار الكود. هذا ليس مجرد توفير وقت، بل هو ضمان أن أي تغيير في نوع الـ Payload سينتشر تلقائياً عبر جميع الـ Actions المرتبطة به، مما يقلل الأخطاء البشرية بشكل كبير.
// مثال متقدم: Generic مع Default Types وConstraints
interface ApiResponse<T, E = Error> {
data?: T;
error?: E;
status: number;
timestamp: Date;
}
// استخدام Constraint لضمان أن T لديه خاصية id
function getEntityById<T extends { id: string }>(id: string): ApiResponse<T> {
return fetch(`/api/entities/${id}`)
.then(res => res.json())
.then(data => ({ data, status: 200, timestamp: new Date() }))
.catch(error => ({ error, status: 500, timestamp: new Date() }));
}
// مثال واقعي: Generic مع Mapped Types
function mapObject<T, U>(obj: T, mapper: (value: T[keyof T]) => U): { [K in keyof T]: U } {
const result = {} as { [K in keyof T]: U };
for (const key in obj) {
result[key] = mapper(obj[key]);
}
return result;
}
// الاستخدام
const user = { name: 'Ahmed', age: 30 };
const userStrings = mapObject(user, value => String(value)); // { name: string, age: string }لاحظ كيف استخدمنا `T extends { id: string }` لضمان أن النوع الممرر لديه خاصية `id` من نوع `string`. هذا ليس مجرد تحقق بسيط، بل هو طريقة لبناء عقود أنواع صارمة تضمن أن الدوال ستتعامل فقط مع البيانات التي تتوقعها. وعندما نضيف Default Types مثل `E = Error`، فإننا نوفر مرونة للمستخدم مع الحفاظ على سلوك افتراضي آمن. هذه هي قوة Generic الحقيقية: ليست مجرد إعادة استخدام، بل هي بناء عقود وأنظمة أنواع متكاملة.
إذا كانت Generic هي الدوال في لغة الأنواع، فإن Conditional Types هي جمل `if-else` التي تسمح لك باتخاذ قرارات بناءً على الأنواع. تخيل أنك تريد بناء نوع `DeepReadonly<T>` يجعل جميع الخصائص في كائن متداخل readonly. بدون Conditional Types، ستضطر لكتابة نوع منفصل لكل مستوى من التداخل، وهو أمر غير عملي. لكن مع Conditional Types، يمكنك كتابة منطق واحد يعالج جميع الحالات بشكل تكراري.
لنأخذ مثالاً من مكتبة React Query. عند تعريف نوع `UseQueryResult<TData, TError>`، يستخدمون Conditional Types لتحديد نوع الـ `data` بناءً على ما إذا كان الاستعلام ناجحاً أم لا. إذا كان الاستعلام في حالة تحميل، فإن `data` يكون `undefined`، وإذا كان هناك خطأ، فإن `data` أيضاً يكون `undefined` لكن الـ `error` يكون موجوداً. هذا النوع من المنطق الديناميكي داخل الأنواع هو ما يميز المكتبات الاحترافية عن تلك التي تعتمد على أنواع ثابتة بسيطة.
// مثال متقدم: Conditional Types مع Recursive Types
// DeepReadonly: يجعل جميع الخصائص في كائن متداخل readonly
// لاحظ استخدام Conditional Type مع infer لفك التعميم
type DeepReadonly<T> = T extends (infer R)[] ? DeepReadonlyArray<R> :
T extends Function ? T :
T extends object ? DeepReadonlyObject<T> : T;
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
// مثال آخر: استخراج نوع معين من Union Type
// ExtractPromiseType: يستخرج نوع القيمة التي يرجعها Promise
type ExtractPromiseType<T> = T extends Promise<infer U> ? U : T;
// الاستخدام
const promise = new Promise<string>(resolve => resolve('Hello'));
type PromiseValue = ExtractPromiseType<typeof promise>; // string
// مثال واقعي: بناء نوع ديناميكي للـ Props بناءً على نوع المكون
type ComponentProps<T> =
T extends React.ComponentType<infer P> ? P :
T extends React.Component<infer P> ? P :
never;
// الاستخدام مع مكون موجود
const MyComponent: React.FC<{ name: string }> = ({ name }) => <div>{name}</div>;
type Props = ComponentProps<typeof MyComponent>; // { name: string }في المثال الأول، استخدمنا `infer R` لاستخراج نوع العناصر من المصفوفة، ثم طبقنا `DeepReadonly` بشكل تكراري على كل عنصر. هذه التقنية تسمح لنا بالتعامل مع أنواع متداخلة بأي عمق دون كتابة كود متكرر. أما في المثال الثاني، فإن `ExtractPromiseType` يستخدم `infer U` لاستخراج نوع القيمة التي يرجعها الـ `Promise`، مما يسمح لنا بكتابة دوال تتعامل مع الـ `Promise` بشكل أكثر أماناً دون الحاجة لفك الـ `Promise` يدوياً.
المشكلة التي يواجهها معظم المطورين مع Conditional Types هي أنهم يحاولون استخدامها لحل مشاكل بسيطة يمكن حلها بـ Union Types أو Mapped Types. لكن القوة الحقيقية لـ Conditional Types تظهر عندما تحتاج لبناء منطق أنواع معقد يعتمد على شروط متعددة. مثلاً، في مكتبة مثل Formik، يستخدمون Conditional Types لتحديد نوع الحقل بناءً على نوع الإدخال: إذا كان الإدخال من نوع `checkbox`، فإن القيمة تكون `boolean`، وإذا كان من نوع `number`، فإن القيمة تكون `number`، وهكذا. هذا النوع من المنطق الديناميكي هو ما يجعل مكتبات مثل Formik قوية ومرنة في نفس الوقت.
الآن بعد أن فهمنا كلاً من Generic وConditional Types على حدة، حان الوقت لنرى كيف يتفاعلان معاً لبناء أنظمة أنواع متكاملة. المثال الكلاسيكي هنا هو مكتبة مثل Zod أو Yup، حيث تستخدم Generic لبناء أنواع ديناميكية تعتمد على الـ Schema، ثم تستخدم Conditional Types لتطبيق التحققات والتحويلات بناءً على نوع البيانات.
تخيل أنك تريد بناء نظام أنواع لـ API يستخرج نوع الـ Response بناءً على الـ Endpoint. يمكنك كتابة Generic مثل `ApiResponse<T>` حيث `T` هو نوع الـ Endpoint، ثم استخدام Conditional Types لتحديد نوع الـ Response بناءً على الـ Method. مثلاً، إذا كان الـ Method هو `GET`، فإن الـ Response يحتوي على `data`، وإذا كان الـ Method هو `POST`، فإن الـ Response يحتوي على `data` و`createdId`. هذا النوع من الأنظمة يسمح لك بكتابة كود آمن تماماً، حيث يخبرك TypeScript إذا حاولت الوصول إلى خاصية غير موجودة بناءً على الـ Method المستخدم.
// مثال متكامل: بناء نظام أنواع لـ API باستخدام Generic وConditional Types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiResponse<T, M extends HttpMethod> =
M extends 'GET' ? { data: T; status: number } :
M extends 'POST' ? { data: T; createdId: string; status: number } :
M extends 'PUT' ? { data: T; updatedId: string; status: number } :
M extends 'DELETE' ? { deletedId: string; status: number } :
never;
// استخدام Generic مع Default Type
function fetchApi<T, M extends HttpMethod = 'GET'>(
url: string,
method: M,
body?: M extends 'GET' ? never : T
): Promise<ApiResponse<T, M>> {
return fetch(url, {
method,
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());
}
// الاستخدام
interface User { id: string; name: string; }
// GET request
fetchApi<User>('/users/1', 'GET').then(res => {
console.log(res.data.name); // TypeScript يعرف أن res يحتوي على data
});
// POST request
fetchApi<User>('/users', 'POST', { name: 'Ahmed' }).then(res => {
console.log(res.createdId); // TypeScript يعرف أن res يحتوي على createdId
});في هذا المثال، استخدمنا Generic `T` لتمثيل نوع البيانات، وGeneric `M` لتمثيل الـ HttpMethod مع Constraint لضمان أنه أحد القيم المسموح بها. ثم استخدمنا Conditional Types لتحديد نوع الـ Response بناءً على الـ Method. لاحظ كيف استخدمنا `M extends 'GET' ? never : T` لضمان أنه لا يمكن تمرير `body` مع الـ `GET` requests، مما يجعل الكود أكثر أماناً. هذا النوع من الأنظمة هو ما يجعل TypeScript أداة قوية ليس فقط للتصحيح، بل لتصميم APIs بشكل صحيح منذ البداية.
عندما تبدأ في استخدام Generic وConditional Types بشكل متقدم، ستواجه مشاكل حقيقية لا تجد لها حلولاً في الوثائق الرسمية. إحدى هذه المشاكل هي ما يسمى بـ "Distributive Conditional Types"، حيث يتم تطبيق Conditional Type على كل عنصر في Union Type بشكل فردي. مثلاً، إذا كان لديك `type T = A | B` و `type U = T extends X ? Y : Z`، فإن TypeScript سيطبق الشرط على `A` و`B` بشكل منفصل، مما قد يؤدي إلى نتائج غير متوقعة.
// مشكلة Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
type StrOrNum = string | number;
type StrOrNumArray = ToArray<StrOrNum>; // string[] | number[]
// الحل: استخدام Tuple لمنع التوزيع
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type StrOrNumArrayN ToArrayNonDistributive<StrOrNum>; // (string | number)[]مشكلة أخرى شائعة هي الأداء. عندما تبدأ في بناء أنواع معقدة باستخدام Generic وConditional Types، قد تلاحظ أن TypeScript يبدأ في التباطؤ، خاصة في المشاريع الكبيرة. السبب هو أن TypeScript يحتاج لمعالجة جميع الأنواع في وقت الترجمة، والأنواع المعقدة تتطلب الكثير من العمليات الحسابية. الحل هنا هو تقليل التعقيد حيثما أمكن، وتجنب الأنواع المتداخلة بشكل مفرط. مثلاً، بدلاً من كتابة `DeepReadonly<T>` لكل شيء، يمكنك استخدامه فقط حيثما تحتاج إليه حقاً.
أخيراً، هناك مشكلة التوازن بين المرونة والأمان. عندما تستخدم Generic وConditional Types بشكل مفرط، قد ينتهي بك الأمر بأنواع مرنة جداً لدرجة أنها لا توفر أي حماية فعلية. مثلاً، إذا كتبت Generic يقبل أي نوع بدون أي Constraints، فإنك تفقد الفائدة الرئيسية من TypeScript وهي اكتشاف الأخطاء في وقت الترجمة. القاعدة الذهبية هنا هي: استخدم Generic حيثما تحتاج لإعادة الاستخدام، واستخدم Constraints حيثما تحتاج لضمان الأمان.
ليس كل مشروع يحتاج إلى Generic وConditional Types المتقدمة. في الواقع، في معظم التطبيقات الصغيرة والمتوسطة، يمكنك الاكتفاء بـ Union Types وMapped Types البسيطة. لكن هناك حالات محددة يجب أن تفكر فيها باستخدام هذه التقنيات:
من ناحية أخرى، يجب أن تتجنب Generic وConditional Types في الحالات التالية:
في رأيي الشخصي، أفضل نهج هو البدء بأنواع بسيطة، ثم الانتقال إلى Generic وConditional Types فقط عندما تشعر بالحاجة الحقيقية إليها. مثلاً، إذا وجدت نفسك تكتب نفس النوع مع اختلافات بسيطة عدة مرات، فهذا مؤشر جيد لاستخدام Generic. وإذا وجدت نفسك بحاجة لمنطق يعتمد على شروط متعددة، فهذا مؤشر لاستخدام Conditional Types. لكن تذكر دائماً: الهدف ليس كتابة أنواع معقدة، بل كتابة أنواع تساعدك على بناء برامج أفضل وأسرع وأكثر أماناً.
إذا أخذت شيئاً واحداً فقط من هذا المقال، فليكن هذا: Generic وConditional Types ليست أدوات لتعقيد الكود، بل هي أدوات لتبسيطه. عندما تستخدمها بشكل صحيح، فإنها تقلل التكرار، تزيد الأمان، وتحسن تجربة المطورين الذين يستخدمون مكتبتك أو يعملون معك في الفريق. لكن المفتاح هو استخدامها بحكمة: ابدأ بأنواع بسيطة، ثم انتقل إلى المتقدم فقط عندما تحتاج حقاً. ولا تنسَ أن الهدف النهائي هو بناء برامج تعمل، وليس كتابة أنواع مثالية.
خطوتك التالية؟ اختر مشكلة حقيقية في مشروعك الحالي حيث تجد نفسك تكتب نفس النوع مع اختلافات بسيطة، وحاول حلها باستخدام Generic. ثم اختر مشكلة أخرى حيث تحتاج لمنطق يعتمد على شروط، وحاول حلها باستخدام Conditional Types. ستجد أن الكود الناتج ليس فقط أقصر، بل أكثر أماناً ومرونة. وعندما تصل إلى هذه المرحلة، ستكون قد انتقلت من مستوى المبتدئ في TypeScript إلى مستوى المحترف الذي يفهم كيف تعمل الأنواع خلف الكواليس.