اكتشف كيف تحول TypeScript من أداة للتحقق من الأنواع إلى لغة برمجة كاملة داخل لغة، باستخدام تقنيات متقدمة مثل Mapped Types وConditional Types التي لا يعرفها إلا المحترفون في شركات مثل مايكروسوفت وجوجل.
في أحد المشاريع الكبيرة التي عملت عليها مع فريق مكون من 12 مطوراً، كنا نستخدم TypeScript لبناء مكتبة مكونات واجهة مستخدم معقدة. المشكلة؟ كانت الأنواع تتكرر في كل مكان، والـ API الخاص بالمكتبة يصبح بطيئاً جداً عند التجميع لأن الـ Type Checker كان ينفذ نفس الحسابات مئات المرات. حينها أدركت أن الـ Generics وحدها ليست كافية — كنا بحاجة إلى شيء أقوى: Conditional Types وMapped Types. هذه التقنيات ليست مجرد مزايا تجميلية؛ إنها تغير طريقة تفكيرك في الأنواع تماماً، وتحول TypeScript من أداة للتحقق من الأنواع إلى لغة برمجة كاملة داخل لغة.
الغريب أن معظم المطورين يتوقفون عند مستوى الـ Generics الأساسي، ويعتقدون أن TypeScript لا يمكن أن يفعل أكثر من ذلك. الحقيقة هي أن TypeScript قادر على تنفيذ منطق معقد داخل نظام الأنواع نفسه، بما في ذلك الحلقات الشرطية والتلاعب الديناميكي بالأنواع. في هذا المقال، سأريك كيف تنتقل من مستوى الـ Generics إلى عالم Conditional Types، وكيف تستخدم هذه التقنيات لحل مشاكل حقيقية في الإنتاج، مثل تحسين أداء الـ Type Checking وتقليل تكرار الكود.
عندما بدأت باستخدام الـ Generics في TypeScript، شعرت وكأنني فتحت صندوق باندورا. فجأة، أصبح بإمكاني كتابة أنواع قابلة لإعادة الاستخدام، مثل `Array<T>` أو `Promise<T>`. لكن سرعان ما اصطدمت بجدار: ماذا لو أردت نوعاً يعتمد على قيمة أخرى؟ مثلاً، نوع `User` الذي يحتوي على حقل `role`، وأريد أن أضيف حقولاً إضافية بناءً على قيمة الـ `role`. الـ Generics الأساسية لا تستطيع التعامل مع هذا النوع من المنطق الشرطي.
لنأخذ مثالاً واقعياً: تخيل أنك تبني نظام إدارة محتوى، ولديك نوع `Content` يحتوي على حقل `type` يمكن أن يكون `article` أو `video` أو `image`. بناءً على قيمة الـ `type`، تريد إضافة حقول مختلفة. مثلاً، إذا كان الـ `type` هو `video`، تريد إضافة حقل `duration`، وإذا كان `image`، تريد إضافة حقل `resolution`. مع الـ Generics الأساسية، ستضطر إلى كتابة ثلاثة أنواع منفصلة، وهذا يعني تكرار الكود وصعوبة الصيانة. هنا يأتي دور Conditional Types.
// مثال بسيط على مشكلة الـ Generics الأساسية
interface BaseContent {
id: string;
title: string;
type: 'article' | 'video' | 'image';
}
// الحل التقليدي: تكرار الأنواع
interface ArticleContent extends BaseContent {
type: 'article';
body: string;
}
interface VideoContent extends BaseContent {
type: 'video';
duration: number;
url: string;
}
interface ImageContent extends BaseContent {
type: 'image';
resolution: string;
url: string;
}
type C ArticleContent | VideoContent | ImageContent;
// المشكلة: ماذا لو أضفنا نوعاً جديداً؟ سنضطر لتكرار كل شيء!Conditional Types هي ميزة قوية في TypeScript تسمح لك بكتابة منطق شرطي داخل نظام الأنواع. الصيغة الأساسية هي `T extends U ? X : Y`، وهي مشابهة تماماً للـ ternary operator في JavaScript. لكن بدلاً من العمل على القيم، تعمل على الأنواع. هذه الميزة تفتح الباب أمام إمكانيات لا حصر لها، مثل إنشاء أنواع ديناميكية تعتمد على قيم أخرى، أو تنفيذ عمليات حسابية على الأنواع.
لنعد إلى مثال `Content`. باستخدام Conditional Types، يمكننا كتابة نوع واحد يعالج كل الحالات بدلاً من تكرار الأنواع. الفكرة هي استخدام نوع وسيط يأخذ قيمة الـ `type` ويحدد الحقول الإضافية بناءً على هذه القيمة. هذا ليس مجرد توفير للكتابة؛ إنه تغيير جوهري في طريقة التفكير في الأنواع. بدلاً من التفكير في الأنواع كهياكل ثابتة، تبدأ في التفكير فيها كدوال يمكنها اتخاذ قرارات بناءً على مدخلات.
// حل باستخدام Conditional Types
interface BaseContent {
id: string;
title: string;
type: 'article' | 'video' | 'image';
}
type ContentType<T extends BaseContent['type']> =
T extends 'article' ? { type: T; body: string } :
T extends 'video' ? { type: T; duration: number; url: string } :
T extends 'image' ? { type: T; resolution: string; url: string } :
never;
type Content<T extends BaseContent['type']> = BaseContent & ContentType<T>;
// الاستخدام
const article: Content<'article'> = {
id: '1',
title: 'TypeScript Advanced Types',
type: 'article',
body: 'محتوى المقال...'
};
const video: Content<'video'> = {
id: '2',
title: 'دليل الفيديو',
type: 'video',
duration: 120,
url: 'https://example.com/video.mp4'
};
// الفائدة: إذا أضفنا نوعاً جديداً، نضيف حالة واحدة فقط في Conditional Type!عندما تكتب `T extends U ? X : Y`، فإن TypeScript لا يقوم بتنفيذ هذا المنطق في وقت التشغيل. بدلاً من ذلك، يعمل الـ Type Checker على تحليل هذا التعبير في وقت التجميع (compile time). ما يحدث هو أن الـ Type Checker يحاول
تحديد ما إذا كان النوع `T` يمكن أن يكون نوعاً فرعياً من `U`. إذا كان كذلك، فإن النوع الناتج هو `X`، وإلا فهو `Y`. هذا التحليل يتم بشكل متكرر حتى يتم الوصول إلى نوع نهائي. مثلاً، إذا كان لديك Conditional Type متداخل، مثل `T extends U ? (T extends V ? A : B) : C`، فإن الـ Type Checker سيحلل كل حالة على حدة.
المشكلة هنا هي أن هذا التحليل يمكن أن يكون مكلفاً من حيث الأداء، خاصة إذا كان لديك أنواع معقدة أو متداخلة. مثلاً، في مكتبة كبيرة مثل React أو Angular، يمكن أن يؤدي استخدام Conditional Types المفرط إلى بطء في عملية التجميع. لذلك، يجب استخدام هذه الميزة بحذر، وتجنب الأنواع المتداخلة بشكل مفرط. في تجربتي، وجدت أن أكثر من ثلاثة مستويات من التداخل يمكن أن يؤدي إلى بطء ملحوظ في الـ Type Checking.
// مثال على Conditional Type معقد قد يؤثر على الأداء
type DeepConditional<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends Array<infer U> ? DeepConditional<U>[] :
T extends object ? { [K in keyof T]: DeepConditional<T[K]> } :
'unknown';
// استخدام
type NestedType = {
a: string;
b: number[];
c: { d: boolean };
};
type Result = DeepConditional<NestedType>;
// النتيجة: { a: 'string'; b: 'number'[]; c: { d: 'boolean' } }
// تحذير: هذا النوع يمكن أن يكون بطيئاً جداً مع أنواع معقدة جداً!إذا كانت Conditional Types تسمح لك باتخاذ قرارات داخل نظام الأنواع، فإن Mapped Types تسمح لك بتحويل الأنواع ديناميكياً. الفكرة الأساسية هي أنك تأخذ نوعاً موجوداً وتطبق عليه تحويلاً معيناً على جميع حقوله. الصيغة الأساسية هي `{ [K in keyof T]: U }`، حيث `T` هو النوع الأصلي، و`K` هو مفتاح كل حقل في `T`، و`U` هو النوع الجديد الذي تريد تطبيقه.
أحد الاستخدامات الشائعة لـ Mapped Types هو إنشاء أنواع جديدة بناءً على أنواع موجودة، مثل تحويل جميع حقول نوع معين إلى أنواع اختيارية أو للقراءة فقط. مثلاً، يمكنك إنشاء نوع `Readonly<T>` أو `Partial<T>` باستخدام Mapped Types. لكن المزايا الحقيقية تظهر عندما تجمع بين Mapped Types وConditional Types لإنشاء أنواع ديناميكية معقدة.
// مثال على Mapped Types الأساسية
interface User {
id: number;
name: string;
email: string;
}
type Read {
readonly [K in keyof User]: User[K];
};
type OptionalUser = {
[K in keyof User]?: User[K];
};
// استخدام
const user: ReadonlyUser = {
id: 1,
name: 'أحمد',
email: 'ahmed@example.com'
};
// user.id = 2; // خطأ: لا يمكن تعديل حقل للقراءة فقط
const optionalUser: OptionalUser = {
name: 'محمد'
}; // الحقول الاختيارية يمكن حذفهاعندما تجمع بين Mapped Types وConditional Types، يمكنك إنشاء أنواع ديناميكية تعتمد على منطق شرطي. مثلاً، يمكنك إنشاء نوع يحول جميع الحقول من نوع `string` إلى `number`، أو يحول جميع الحقول الاختيارية إلى حقول إلزامية. هذا النوع من التلاعب بالأنواع يمكن أن يكون مفيداً جداً في مكتبات مثل Redux أو GraphQL، حيث تحتاج إلى تحويل أنواع البيانات بين طبقات مختلفة من التطبيق.
لنأخذ مثالاً عملياً: تخيل أنك تبني مكتبة لإدارة النماذج، وتريد إنشاء نوع جديد يحول جميع الحقول من نوع `string` إلى حقول يمكن أن تكون `string` أو `null`، بينما يترك الحقول من الأنواع الأخرى كما هي. باستخدام Mapped Types وConditional Types، يمكنك كتابة نوع واحد يفعل ذلك بدلاً من تعديل كل حقل يدوياً.
// مثال على الجمع بين Mapped Types وConditional Types
interface Form {
name: string;
age: number;
isActive: boolean;
metadata?: object;
}
type NullableStrings<T> = {
[K in keyof T]: T[K] extends string ? string | null : T[K];
};
type NullableForm = NullableStrings<Form>;
// النتيجة: {
// name: string | null;
// age: number;
// isActive: boolean;
// metadata?: object;
// }
// استخدام
const form: NullableForm = {
name: null,
age: 30,
isActive: true
}; // name يمكن أن يكون null الآنالكلمة المفتاحية `infer` هي واحدة من أقوى الميزات في TypeScript، لكنها غالباً ما تُغفل. تسمح لك `infer` باستخراج أنواع من أنواع مركبة داخل Conditional Types. مثلاً، يمكنك استخراج نوع العنصر من مصفوفة، أو نوع القيمة من `Promise`، أو حتى نوع المفتاح من نوع معين. هذه الميزة مفيدة جداً عندما تعمل مع أنواع مركبة مثل `Array` أو `Promise` أو `Record`.
لنأخذ مثالاً بسيطاً: تخيل أنك تريد كتابة نوع يستخرج نوع العنصر من مصفوفة. باستخدام `infer`، يمكنك كتابة نوع واحد يفعل ذلك بدلاً من الاعتماد على الأنواع المعرفة مسبقاً مثل `T[number]` (التي تعمل فقط مع المصفوفات). الفكرة هي أنك تستخدم Conditional Type مع `infer` لاستخراج النوع الذي تريده.
// مثال على استخدام infer لاستخراج أنواع من أنواع مركبة
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type PromiseValue<T> = T extends Promise<infer U> ? U : never;
type FunctionReturn<T> = T extends (...args: any[]) => infer U ? U : never;
// استخدام
const numbers = [1, 2, 3];
type NumberType = ArrayElement<typeof numbers>; // number
const promise = Promise.resolve('hello');
type PromiseType = PromiseValue<typeof promise>; // string
function greet(): string {
return 'hello';
}
type GreetReturn = FunctionReturn<typeof greet>; // stringيمكن استخدام `infer` في سيناريوهات أكثر تعقيداً، مثل استخراج أنواع من أنواع متداخلة أو حتى تنفيذ عمليات حسابية على الأنواع. مثلاً، يمكنك كتابة نوع يستخرج جميع أنواع `Promise` من نوع مركب، أو نوع يستخرج جميع الحقول من نوع معين التي تكون من نوع `string`. هذه الأنواع من التلاعب بالأنواع يمكن أن تكون مفيدة جداً في مكتبات مثل Redux-Saga أو Apollo Client، حيث تحتاج إلى التعامل مع أنواع معقدة ومتداخلة.
في أحد المشاريع، كنت بحاجة إلى كتابة نوع يستخرج جميع الحقول من نوع معين التي تكون من نوع `Observable` (من مكتبة RxJS). باستخدام `infer`، تمكنت من كتابة نوع واحد يفعل ذلك بدلاً من الاعتماد على التعليقات التوضيحية أو الوثائق. هذا النوع من الحلول ليس مجرد توفير للكتابة؛ إنه يجعل الكود أكثر أماناً وصيانة، لأن الـ Type Checker سيتأكد من أن جميع الحقول من نوع `Observable` تعامل بشكل صحيح.
// مثال على استخدام infer لاستخراج أنواع معينة من نوع مركب
import { Observable } from 'rxjs';
interface Data {
id: number;
name: Observable<string>;
age: number;
metadata: Observable<object>;
}
type ExtractObservables<T> = {
[K in keyof T]: T[K] extends Observable<infer U> ? U : never;
};
type ObservableTypes = ExtractObservables<Data>;
// النتيجة: {
// id: never;
// name: string;
// age: never;
// metadata: object;
// }
// استخدام متقدم: استخراج جميع أنواع Observable من نوع مركب
interface ComplexData {
user: {
name: Observable<string>;
age: number;
};
posts: Observable<Array<{ title: string }>>;
}
type DeepExtractObservables<T> =
T extends Observable<infer U> ? DeepExtractObservables<U> :
T extends object ? { [K in keyof T]: DeepExtractObservables<T[K]> } :
T;
type ComplexObservableTypes = DeepExtractObservables<ComplexData>;
// النتيجة: {
// user: {
// name: string;
// age: number;
// };
// posts: Array<{ title: string }>;
// }رغم قوة هذه التقنيات، إلا أنها تأتي مع تحدياتها الخاصة. أحد أكبر التحديات هو الأداء. كما ذكرت سابقاً، الأنواع المعقدة يمكن أن تؤدي إلى بطء في عملية التجميع. في أحد المشاريع، كان لدينا نوع يستخدم Conditional Types متداخلة بعمق 5 مستويات، وكان الـ Type Checking يستغرق أكثر من 30 ثانية في كل مرة نقوم فيها بتغيير بسيط في الكود. الحل؟ قمنا بتقسيم النوع إلى أنواع أصغر واستخدمنا أنواع وسيطة لتقليل التعقيد.
تحدي آخر هو قابلية القراءة. الأنواع المعقدة يمكن أن تكون صعبة الفهم، خاصة للمطورين الجدد في الفريق. مثلاً، نوع مثل `DeepExtractObservables` في المثال السابق قد يكون صعباً على المطورين الذين لم يعتادوا على هذه التقنيات. لذلك، من المهم توثيق هذه الأنواع جيداً واستخدام أسماء واضحة. في تجربتي، وجدت أن إضافة تعليقات توضيحية فوق الأنواع المعقدة يمكن أن يوفر ساعات من الوقت في المستقبل عندما يعود المطورون إلى الكود.
على الرغم من قوة هذه التقنيات، إلا أنها ليست الحل الأمثل في جميع الحالات. مثلاً، إذا كنت تعمل على مشروع صغير أو متوسط الحجم، فقد لا تحتاج إلى هذه التعقيدات. في الواقع، يمكن أن تؤدي إلى تعقيد الكود بدون فائدة حقيقية. أيضاً، إذا كان فريقك غير معتاد على هذه التقنيات، فقد يكون من الأفضل تجنبها للحفاظ على قابلية الصيانة.
في أحد المشاريع الصغيرة، حاولت استخدام Conditional Types لإنشاء أنواع ديناميكية للمكونات في واجهة المستخدم. لكن سرعان ما أدركت أن الكود أصبح صعب الفهم، وأن الفوائد كانت ضئيلة مقارنة بالتعقيد المضاف. لذلك، قررت العودة إلى الأنواع البسيطة، وكان القرار صحيحاً. القاعدة الذهبية هنا هي: استخدم هذه التقنيات فقط عندما تكون هناك حاجة حقيقية لها، وليس لمجرد أنها متاحة.
إذا أخذت شيئاً واحداً من هذا المقال، فليكن هذا: الأنواع في TypeScript ليست مجرد أداة للتحقق من الأخطاء؛ إنها لغة برمجة كاملة داخل لغة. عندما تبدأ في التفكير في الأنواع كدوال يمكنها اتخاذ قرارات وتنفيذ منطق، ستغير طريقة كتابتك للكود تماماً. بدلاً من كتابة أنواع ثابتة، ستبدأ في كتابة أنواع ديناميكية تتكيف مع السياق، وهذا سيجعل كودك أكثر مرونة وصيانة.
ابدأ بتطبيق هذه التقنيات في مشروعك الحالي. ابحث عن الأماكن التي تتكرر فيها الأنواع، أو الأماكن التي تعتمد على منطق شرطي، وحاول استبدالها بأنواع ديناميكية. ستجد أن الكود يصبح أكثر نظافة، وأنك تقلل من احتمالية الأخطاء. لكن تذكر: لا تبالغ في التعقيد. استخدم هذه التقنيات بحكمة، وعندما تشعر أن الكود أصبح صعب الفهم، توقف واسأل نفسك: هل هذا ضروري حقاً؟