دليل شامل للأنواع المحوّلة والأنواع الشرطية القوية في TypeScript، يتضمن أمثلة عملية وحالات استخدام متقدمة لإنشاء تطبيقات قوية وآمنة من حيث النوع.
إتقان الأنواع المحوّلة والأنواع الشرطية في TypeScript
تقدم TypeScript، وهي مجموعة شاملة من JavaScript، ميزات قوية لإنشاء تطبيقات متينة وقابلة للصيانة. ومن بين هذه الميزات، تبرز الأنواع المحوّلة (Mapped Types) والأنواع الشرطية (Conditional Types) كأدوات أساسية لمعالجة الأنواع المتقدمة. يقدم هذا الدليل نظرة شاملة على هذه المفاهيم، مستكشفًا صيغتها وتطبيقاتها العملية وحالات الاستخدام المتقدمة. سواء كنت مطور TypeScript متمرسًا أو بدأت رحلتك للتو، سيزودك هذا المقال بالمعرفة اللازمة للاستفادة من هذه الميزات بفعالية.
ما هي الأنواع المحوّلة (Mapped Types)؟
تسمح لك الأنواع المحوّلة بإنشاء أنواع جديدة عن طريق تحويل الأنواع الموجودة. فهي تتكرر على خصائص نوع موجود وتطبق تحويلاً على كل خاصية. وهذا مفيد بشكل خاص لإنشاء تنويعات من الأنواع الموجودة، مثل جعل جميع الخصائص اختيارية أو للقراءة فقط.
الصيغة الأساسية
صيغة النوع المحوّل هي كما يلي:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: النوع المُدخل الذي تريد تحويله.K in keyof T
: يتكرر على كل مفتاح في النوع المُدخلT
. ينشئkeyof T
اتحادًا (union) لجميع أسماء الخصائص فيT
، ويمثلK
كل مفتاح فردي أثناء التكرار.Transformation
: التحويل الذي تريد تطبيقه على كل خاصية. قد يكون ذلك إضافة مُعدِّل (مثلreadonly
أو?
)، أو تغيير النوع، أو أي شيء آخر تمامًا.
أمثلة عملية
جعل الخصائص للقراءة فقط
لنفترض أن لديك واجهة (interface) تمثل ملف تعريف مستخدم:
interface UserProfile {
name: string;
age: number;
email: string;
}
يمكنك إنشاء نوع جديد تكون فيه جميع الخصائص للقراءة فقط:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
الآن، سيكون للنوع ReadOnlyUserProfile
نفس خصائص UserProfile
، ولكنها جميعها ستكون للقراءة فقط.
جعل الخصائص اختيارية
بالمثل، يمكنك جعل جميع الخصائص اختيارية:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
سيكون للنوع OptionalUserProfile
جميع خصائص UserProfile
، ولكن كل خاصية ستكون اختيارية.
تعديل أنواع الخصائص
يمكنك أيضًا تعديل نوع كل خاصية. على سبيل المثال، يمكنك تحويل جميع الخصائص لتكون من نوع سلسلة نصية (string):
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
في هذه الحالة، ستكون جميع الخصائص في StringifiedUserProfile
من النوع string
.
ما هي الأنواع الشرطية (Conditional Types)؟
تسمح لك الأنواع الشرطية بتعريف أنواع تعتمد على شرط معين. فهي توفر طريقة للتعبير عن العلاقات بين الأنواع بناءً على ما إذا كان النوع يفي بشرط معين. هذا مشابه للعامل الثلاثي (ternary operator) في JavaScript، ولكنه للأنواع.
الصيغة الأساسية
صيغة النوع الشرطي هي كما يلي:
T extends U ? X : Y
T
: النوع الذي يتم التحقق منه.U
: النوع الذي يمتد منهT
(الشرط).X
: النوع الذي سيتم إرجاعه إذا كانT
يمتد منU
(الشرط صحيح).Y
: النوع الذي سيتم إرجاعه إذا لم يكنT
يمتد منU
(الشرط خاطئ).
أمثلة عملية
تحديد ما إذا كان النوع سلسلة نصية (string)
لنقم بإنشاء نوع يُرجع string
إذا كان النوع المُدخل سلسلة نصية، وnumber
في الحالات الأخرى:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
استخراج نوع من اتحاد (Union)
يمكنك استخدام الأنواع الشرطية لاستخراج نوع معين من نوع اتحادي (union type). على سبيل المثال، لاستخراج الأنواع غير القابلة للقيم الفارغة (non-nullable):
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
هنا، إذا كان T
هو null
أو undefined
، يصبح النوع never
، والذي يتم تصفيته بعد ذلك بواسطة تبسيط أنواع الاتحاد في TypeScript.
استنتاج الأنواع (Inferring Types)
يمكن أيضًا استخدام الأنواع الشرطية لاستنتاج الأنواع باستخدام الكلمة المفتاحية infer
. وهذا يسمح لك باستخراج نوع من بنية نوع أكثر تعقيدًا.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
في هذا المثال، يستخرج ReturnType
نوع القيمة المرجعة من دالة. يتحقق مما إذا كانت T
دالة تأخذ أي وسائط وترجع نوعًا R
. إذا كان الأمر كذلك، فإنه يُرجع R
؛ وإلا فإنه يُرجع any
.
الجمع بين الأنواع المحوّلة والأنواع الشرطية
تكمن القوة الحقيقية للأنواع المحوّلة والأنواع الشرطية في الجمع بينهما. وهذا يسمح لك بإنشاء تحويلات أنواع مرنة ومعبرة للغاية.
مثال: Deep Readonly
إحدى حالات الاستخدام الشائعة هي إنشاء نوع يجعل جميع خصائص الكائن، بما في ذلك الخصائص المتداخلة، للقراءة فقط. يمكن تحقيق ذلك باستخدام نوع شرطي تعاودي (recursive).
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
هنا، يطبق DeepReadonly
بشكل تعاودي المُعدِّل readonly
على جميع الخصائص وخصائصها المتداخلة. إذا كانت الخاصية كائنًا، فإنه يستدعي DeepReadonly
بشكل تعاودي على ذلك الكائن. وإلا، فإنه يطبق ببساطة المُعدِّل readonly
على الخاصية.
مثال: تصفية الخصائص حسب النوع
لنفترض أنك تريد إنشاء نوع يتضمن فقط الخصائص من نوع معين. يمكنك الجمع بين الأنواع المحوّلة والأنواع الشرطية لتحقيق ذلك.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
في هذا المثال، يتكرر FilterByType
على خصائص T
ويتحقق مما إذا كان نوع كل خاصية يمتد من U
. إذا كان الأمر كذلك، فإنه يدرج الخاصية في النوع الناتج؛ وإلا، فإنه يستبعدها عن طريق تحويل المفتاح إلى never
. لاحظ استخدام "as" لإعادة تعيين المفاتيح. ثم نستخدم `Omit` و `keyof StringProperties` لإزالة الخصائص من النوع سلسلة نصية من الواجهة الأصلية.
حالات الاستخدام والأنماط المتقدمة
بالإضافة إلى الأمثلة الأساسية، يمكن استخدام الأنواع المحوّلة والأنواع الشرطية في سيناريوهات أكثر تقدمًا لإنشاء تطبيقات قابلة للتخصيص بدرجة عالية وآمنة من حيث النوع.
الأنواع الشرطية التوزيعية
تكون الأنواع الشرطية توزيعية عندما يكون النوع الذي يتم التحقق منه نوعًا اتحاديًا (union type). هذا يعني أن الشرط يتم تطبيقه على كل عضو في الاتحاد بشكل فردي، ثم يتم دمج النتائج في نوع اتحادي جديد.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
في هذا المثال، يتم تطبيق ToArray
على كل عضو في الاتحاد string | number
بشكل فردي، مما ينتج عنه string[] | number[]
. إذا لم يكن الشرط توزيعيًا، لكانت النتيجة (string | number)[]
.
استخدام الأنواع المساعدة (Utility Types)
توفر TypeScript العديد من الأنواع المساعدة المدمجة التي تستفيد من الأنواع المحوّلة والأنواع الشرطية. يمكن استخدام هذه الأنواع المساعدة كعناصر بناء لتحويلات أنواع أكثر تعقيدًا.
Partial<T>
: يجعل جميع خصائصT
اختيارية.Required<T>
: يجعل جميع خصائصT
مطلوبة.Readonly<T>
: يجعل جميع خصائصT
للقراءة فقط.Pick<T, K>
: يختار مجموعة من الخصائصK
منT
.Omit<T, K>
: يزيل مجموعة من الخصائصK
منT
.Record<K, T>
: ينشئ نوعًا بمجموعة من الخصائصK
من النوعT
.Exclude<T, U>
: يستبعد منT
جميع الأنواع التي يمكن تعيينها إلىU
.Extract<T, U>
: يستخرج منT
جميع الأنواع التي يمكن تعيينها إلىU
.NonNullable<T>
: يستبعدnull
وundefined
منT
.Parameters<T>
: يحصل على معاملات نوع الدالةT
.ReturnType<T>
: يحصل على نوع القيمة المرجعة لنوع الدالةT
.InstanceType<T>
: يحصل على نوع النسخة (instance) لنوع الدالة المُنشِئةT
.
هذه الأنواع المساعدة هي أدوات قوية يمكنها تبسيط معالجة الأنواع المعقدة. على سبيل المثال، يمكنك دمج Pick
و Partial
لإنشاء نوع يجعل خصائص معينة فقط اختيارية:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
في هذا المثال، يحتوي OptionalDescriptionProduct
على جميع خصائص Product
، ولكن خاصية description
اختيارية.
استخدام أنواع القوالب الحرفية (Template Literal Types)
تسمح لك أنواع القوالب الحرفية بإنشاء أنواع بناءً على السلاسل النصية الحرفية. يمكن استخدامها بالاقتران مع الأنواع المحوّلة والأنواع الشرطية لإنشاء تحويلات أنواع ديناميكية ومعبرة. على سبيل المثال، يمكنك إنشاء نوع يضيف بادئة معينة لجميع أسماء الخصائص:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
في هذا المثال، سيكون للنوع PrefixedSettings
خصائص data_apiUrl
و data_timeout
.
أفضل الممارسات والاعتبارات
- حافظ على البساطة: على الرغم من أن الأنواع المحوّلة والأنواع الشرطية قوية، إلا أنها يمكن أن تجعل شفرتك أكثر تعقيدًا. حاول إبقاء تحويلات الأنواع الخاصة بك بسيطة قدر الإمكان.
- استخدم الأنواع المساعدة: استفد من الأنواع المساعدة المدمجة في TypeScript كلما أمكن ذلك. فهي مختبرة جيدًا ويمكن أن تبسط شفرتك.
- وثّق أنواعك: وثّق تحويلات الأنواع الخاصة بك بوضوح، خاصة إذا كانت معقدة. سيساعد هذا المطورين الآخرين على فهم شفرتك.
- اختبر أنواعك: استخدم مدقق الأنواع في TypeScript للتأكد من أن تحويلات الأنواع الخاصة بك تعمل كما هو متوقع. يمكنك كتابة اختبارات وحدة للتحقق من سلوك أنواعك.
- ضع في اعتبارك الأداء: يمكن أن تؤثر تحويلات الأنواع المعقدة على أداء مترجم TypeScript. كن على دراية بمدى تعقيد أنواعك وتجنب الحسابات غير الضرورية.
الخاتمة
تعتبر الأنواع المحوّلة (Mapped Types) والأنواع الشرطية (Conditional Types) ميزات قوية في TypeScript تمكنك من إنشاء تحويلات أنواع مرنة ومعبرة للغاية. من خلال إتقان هذه المفاهيم، يمكنك تحسين أمان النوع وقابلية الصيانة والجودة الشاملة لتطبيقات TypeScript الخاصة بك. من التحويلات البسيطة مثل جعل الخصائص اختيارية أو للقراءة فقط إلى التحويلات التعاودية المعقدة والمنطق الشرطي، توفر هذه الميزات الأدوات التي تحتاجها لبناء تطبيقات قوية وقابلة للتطوير. استمر في استكشاف وتجربة هذه الميزات لإطلاق العنان لإمكاناتها الكاملة وتصبح مطور TypeScript أكثر كفاءة.
بينما تواصل رحلتك مع TypeScript، تذكر الاستفادة من ثروة الموارد المتاحة، بما في ذلك وثائق TypeScript الرسمية والمجتمعات عبر الإنترنت والمشاريع مفتوحة المصدر. احتضن قوة الأنواع المحوّلة والأنواع الشرطية، وستكون مجهزًا جيدًا لمواجهة حتى أكثر المشكلات المتعلقة بالأنواع تحديًا.