هل تعتقد أن Generic Types هي قمة TypeScript؟ اكتشف كيف تحول Conditional Types الأنواع إلى منطق برمجي حي، وكيف يستخدمها المحترفون لكتابة كود لا يكسره شيء حتى في أكبر المشاريع.
في يوم من الأيام، كنت أعمل على مكتبة مكونات واجهة مستخدم ضخمة لشركة ناشئة في دبي. المكتبة كانت تدعم 12 لغة، وكل مكون كان له 4 حالات مختلفة (default, hover, active, disabled). المشكلة؟ كان لدينا 72 نوع مختلف فقط للمكونات الأساسية. عندما أضفنا الـ props الديناميكية، وصل عدد الأنواع إلى 288 نوع. المطورون الجدد كانوا يكسرون الكود كل يوم لأنهم لا يعرفون بالضبط ما يجب تمريره لكل حالة. هنا أدركت أن Generic Types وحدها لن تنقذنا — كنا بحاجة لشيء أقوى: Conditional Types.
الـ TypeScript ليس مجرد أداة لتحسين الـ autocomplete في المحرر. عندما تتقن Advanced Types، تتحول الأنواع من مجرد ملاحظات إلى منطق برمجي كامل. تخيل أنك تستطيع كتابة دالة لا تحدد فقط نوع البيانات الذي تقبله، بل تحدد أيضاً كيف سيتغير هذا النوع بناءً على المدخلات. هذا بالضبط ما تفعله Conditional Types. إنها تسمح لك بكتابة كود يقول: "إذا كان النوع هو A، فاستخدم B، وإلا فاستخدم C" — تماماً كما تفعل مع الشروط في الكود العادي، لكن على مستوى الأنواع.
الـ Generic Types هي أول خطوة حقيقية نحو كتابة كود مرن وقابل لإعادة الاستخدام. لكنها في الحقيقة مجرد أداة بسيطة مقارنة بما يمكن فعله. عندما تستخدم Generic، فأنت تقول للـ TypeScript: "هذا النوع غير محدد الآن، لكن سأخبره لاحقاً". هذا مفيد جداً عندما تريد كتابة دالة أو مكون يقبل أنواع مختلفة من البيانات دون تكرار الكود. مثلاً، دالة identity التي تعيد نفس القيمة التي تستقبلها:
function identity<T>(arg: T): T {
return arg;
}
// الاستخدام
const num = identity<number>(42); // number
const str = identity<string>('hello'); // stringلكن المشكلة تظهر عندما تريد فعل شيء أكثر تعقيداً. ماذا لو أردت كتابة دالة تجمع بين نوعين مختلفين، وتعيد نوعاً جديداً بناءً على مدخلات معينة؟ هنا تبدأ الأمور تصبح معقدة. مثلاً، تخيل أنك تريد كتابة دالة تجمع بين objectين، وتعيد object جديد يحتوي على جميع الخصائص، لكن مع تغيير بعض الخصائص بناءً على نوع معين. Generic Types وحدها لن تساعدك هنا — تحتاج إلى شيء أكثر ذكاءً.
الـ Conditional Types هي حيث تبدأ المتعة الحقيقية. إنها تسمح لك بكتابة تعبيرات شرطية داخل الأنواع نفسها. الصيغة الأساسية هي T extends U ? X : Y، والتي تعني: "إذا كان النوع T يمتد من النوع U، فاستخدم النوع X، وإلا فاستخدم النوع Y". هذا يبدو بسيطاً، لكنه يفتح باباً واسعاً لإمكانيات لا محدودة. مثلاً، يمكنك كتابة نوع يحدد ما إذا كان نوع معين هو array أم لا:
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<string>; // falseلكن القوة الحقيقية تظهر عندما تجمع بين Conditional Types و Generic Types. مثلاً، يمكنك كتابة نوع يستخرج نوع العناصر من array، أو حتى نوع مفاتيح object التي قيمتها من نوع معين. هذا بالضبط ما تفعله مكتبة مثل React مع الـ props الخاصة بالمكونات. تخيل أنك تريد كتابة نوع يستخرج فقط الخصائص التي قيمتها من نوع function من object معين:
type FunctionProperties<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
interface Person {
name: string;
age: number;
greet: () => void;
walk: () => void;
}
type Pers FunctionProperties<Person>; // "greet" | "walk"هذه التقنية تستخدم في مكتبات كبيرة مثل Redux و Vuex لتحديد الـ actions التي يمكن إرسالها إلى الـ store. بدلاً من كتابة كل نوع يدوياً، يمكنك استخدام Conditional Types لاستخراج الأنواع تلقائياً من object معين. هذا يقلل من الأخطاء ويجعل الكود أكثر قابلية للصيانة.
الكلمة المفتاحية infer هي أحد أقوى الأدوات في TypeScript، لكنها غالباً ما تُفهم بشكل خاطئ. إنها تسمح لك باستخراج نوع من نوع آخر داخل Conditional Types. مثلاً، يمكنك استخدامها لاستخراج نوع العناصر من array، أو نوع القيمة المرجعة من دالة. هذا يبدو بسيطاً، لكنه يفتح إمكانيات هائلة. فكر في الأمر كطريقة لـ "التقاط" نوع معين من نوع أكبر.
type ExtractArrayType<T> = T extends (infer U)[] ? U : never;
type Numbers = ExtractArrayType<number[]>; // number
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type GreetReturn = ReturnType<() => string>; // stringهذه التقنية تستخدم بشكل مكثف في مكتبات مثل Zod و io-ts لتحليل الأنواع المعقدة. مثلاً، يمكنك كتابة نوع يستخرج نوع معين من object متداخل، أو حتى نوع معين من union type. المشكلة هي أن infer يمكن أن يكون صعب الفهم في البداية، خاصة عندما تستخدمه داخل أنواع متداخلة. لكن بمجرد أن تفهمه، ستجد نفسك تستخدمه في كل مكان.
في أحد المشاريع التي عملت عليها، كان لدينا API يعيد responses معقدة للغاية. كل response كان يحتوي على data و meta و errors، وكل منها كان يمكن أن يكون من أنواع مختلفة بناءً على الـ endpoint. بدلاً من كتابة أنواع يدوية لكل response، استخدمنا Conditional Types مع infer لاستخراج الأنواع تلقائياً:
type ApiResponse<T> = {
data: T;
meta?: {
pagination?: {
total: number;
page: number;
perPage: number;
};
};
errors?: Array<{
message: string;
code: string;
}>;
};
type ExtractDataType<T> = T extends ApiResponse<infer U> ? U : never;
// مثال على استخدام
interface User {
id: string;
name: string;
email: string;
}
type UserResp ApiResponse<User[]>;
type Users = ExtractDataType<UserResponse>; // User[]هذه التقنية وفرت علينا ساعات من العمل اليدوي، وجعلت الكود أكثر أماناً. عندما يتغير شكل الـ API، تتغير الأنواع تلقائياً دون الحاجة لتحديثها يدوياً. هذا بالضبط نوع الفائدة التي تجعل Advanced Types تستحق الوقت المستثمر في تعلمها.
المطورون غالباً ما يتجاهلون Mapped Types و Template Literal Types، لكنهم في الحقيقة أداتان قويتان جداً عندما تجمع بينهما وبين Conditional Types. Mapped Types تسمح لك بإنشاء أنواع جديدة بناءً على أنواع موجودة، بينما Template Literal Types تسمح لك بإنشاء أنواع جديدة من strings ديناميكية. مثلاً، يمكنك استخدام Mapped Types لتحويل جميع خصائص object إلى optional:
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface Person {
name: string;
age: number;
}
type Opti Optional<Person>; // { name?: string; age?: number }لكن القوة الحقيقية تظهر عندما تجمع بين Mapped Types و Conditional Types. مثلاً، يمكنك كتابة نوع يحول جميع الخصائص من نوع string إلى نوع number، بينما يترك الخصائص الأخرى كما هي:
type StringToNumber<T> = {
[K in keyof T]: T[K] extends string ? number : T[K];
};
interface Person {
name: string;
age: number;
isActive: boolean;
}
type NumericPerson = StringToNumber<Person>; // { name: number; age: number; isActive: boolean }Template Literal Types تضيف بعداً آخر من المرونة. يمكنك استخدامها لإنشاء أنواع ديناميكية بناءً على strings. مثلاً، يمكنك كتابة نوع يولد جميع الـ event names الممكنة لنوع معين من المكونات:
type EventName<T extends string> = `on${Capitalize<T>}`;
type Butt EventName<'click' | 'hover' | 'focus'>;
// "onClick" | "onHover" | "onFocus"هذه التقنية تستخدم في مكتبات مثل React لتحديد الـ props الخاصة بالأحداث. بدلاً من كتابة كل نوع يدوياً، يمكنك استخدام Template Literal Types لإنشاء الأنواع تلقائياً بناءً على أسماء الأحداث.
رغم قوة Advanced Types، إلا أنها تأتي مع مجموعة من الفخاخ التي يقع فيها حتى المطورون المحترفون. المشكلة الأكبر هي الأداء. عندما تبدأ في كتابة أنواع معقدة للغاية، يمكن أن يبطئ ذلك الـ compilation بشكل كبير. مثلاً، أنواع تحتوي على عدة مستويات من Conditional Types المتداخلة يمكن أن تجعل الـ TypeScript يستغرق دقائق بدلاً من ثوانٍ لبناء المشروع. هذا ليس مجرد إزعاج — إنه يمكن أن يدمر إنتاجية الفريق بأكمله.
مشكلة أخرى شائعة هي التعقيد الزائد. بعض المطورين يحاولون كتابة أنواع تفعل كل شيء، مما يجعل الكود غير قابل للقراءة. مثلاً، رأيت مرة نوعاً مكوناً من 50 سطر يحاول فعل كل شيء بدءاً من تحليل الـ API responses وحتى تحويل أنواع البيانات. النتيجة؟ لا أحد في الفريق يستطيع فهمه، وكل تغيير صغير يكسر كل شيء. القاعدة الذهبية هنا هي: إذا كان النوع أكثر تعقيداً من الكود الذي يستخدمه، فأنت تفعل شيئاً خاطئاً.
هناك حالات معينة يجب فيها تجنب استخدام Advanced Types تماماً. مثلاً، في المشاريع الصغيرة أو السريعة، قد لا تستحق الوقت المستثمر في كتابة أنواع معقدة. أيضاً، إذا كان فريقك غير متمكن من TypeScript، فقد يكون من الأفضل استخدام أنواع أبسط وأسهل للفهم. لكن الأهم من ذلك هو تجنب استخدامها عندما لا تضيف قيمة حقيقية. مثلاً، إذا كان لديك object بسيط يحتوي على 3 خصائص، فلا داعي لكتابة أنواع معقدة لاستخراجه.
إذا كنت تريد البدء في استخدام Advanced Types في مشاريعك، فإليك الخطوات العملية التي أوصي بها. أولاً، ابدأ بكتابة أنواع بسيطة باستخدام Generic Types. مثلاً، اكتب دالة تقبل نوعاً عاماً وتعيد نوعاً عاماً. بعد ذلك، جرب كتابة أنواع بسيطة باستخدام Conditional Types. مثلاً، اكتب نوع يحدد ما إذا كان نوع معين هو string أم لا. بعد أن تتقن الأساسيات، انتقل إلى استخدام infer لاستخراج الأنواع من أنواع أخرى.
المفتاح هنا هو الممارسة. لا تحاول كتابة أنواع معقدة من البداية. ابدأ بأمثلة بسيطة، ثم قم بزيادة التعقيد تدريجياً. أيضاً، لا تخف من قراءة الكود المصدر لمكتبات مثل React و Redux. هذه المكتبات تستخدم Advanced Types بشكل مكثف، ويمكنك تعلم الكثير من قراءة الكود الخاص بها.
// تمرين عملي: اكتب نوع يستخرج جميع الخصائص من نوع معين التي قيمتها من نوع function
type FunctionProperties<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface Example {
name: string;
age: number;
greet: () => void;
walk: () => void;
sleep: () => void;
}
// يجب أن يكون النتيجة: "greet" | "walk" | "sleep"
type ExampleFuncti FunctionProperties<Example>;أخيراً، تذكر أن الهدف من Advanced Types ليس كتابة كود معقد، بل كتابة كود أكثر أماناً وقابلية للصيانة. إذا وجدت نفسك تكتب نوعاً لا يفهمه أحد في الفريق، فربما تحتاج إلى إعادة التفكير في تصميمك. الأنواع يجب أن تساعد المطورين، وليس تعقد عليهم الأمور.
الـ Advanced Types في TypeScript ليست مجرد ميزة جميلة لإبهار زملائك. إنها أداة قوية يمكن أن تحول الكود الخاص بك من كود قابل للكسر إلى كود لا يمكن كسره تقريباً. لكن مثل أي أداة قوية، يجب استخدامها بحكمة. ابدأ بالأساسيات، ثم قم بزيادة التعقيد تدريجياً. تذكر دائماً: الهدف هو كتابة كود أكثر أماناً وأسهل للصيانة، وليس كتابة أنواع معقدة فقط لإثبات أنك تستطيع ذلك. إذا استخدمت Advanced Types بشكل صحيح، ستجد أن الكود الخاص بك يصبح أكثر مرونة، وأقل عرضة للأخطاء، وأسهل للفهم — وهذا بالضبط ما تريده أي شركة برمجيات ناجحة.