العربية

دليل شامل للأنواع المحوّلة والأنواع الشرطية القوية في TypeScript، يتضمن أمثلة عملية وحالات استخدام متقدمة لإنشاء تطبيقات قوية وآمنة من حيث النوع.

إتقان الأنواع المحوّلة والأنواع الشرطية في TypeScript

تقدم TypeScript، وهي مجموعة شاملة من JavaScript، ميزات قوية لإنشاء تطبيقات متينة وقابلة للصيانة. ومن بين هذه الميزات، تبرز الأنواع المحوّلة (Mapped Types) والأنواع الشرطية (Conditional Types) كأدوات أساسية لمعالجة الأنواع المتقدمة. يقدم هذا الدليل نظرة شاملة على هذه المفاهيم، مستكشفًا صيغتها وتطبيقاتها العملية وحالات الاستخدام المتقدمة. سواء كنت مطور TypeScript متمرسًا أو بدأت رحلتك للتو، سيزودك هذا المقال بالمعرفة اللازمة للاستفادة من هذه الميزات بفعالية.

ما هي الأنواع المحوّلة (Mapped Types)؟

تسمح لك الأنواع المحوّلة بإنشاء أنواع جديدة عن طريق تحويل الأنواع الموجودة. فهي تتكرر على خصائص نوع موجود وتطبق تحويلاً على كل خاصية. وهذا مفيد بشكل خاص لإنشاء تنويعات من الأنواع الموجودة، مثل جعل جميع الخصائص اختيارية أو للقراءة فقط.

الصيغة الأساسية

صيغة النوع المحوّل هي كما يلي:

type NewType<T> = {
  [K in keyof T]: Transformation;
};

أمثلة عملية

جعل الخصائص للقراءة فقط

لنفترض أن لديك واجهة (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

أمثلة عملية

تحديد ما إذا كان النوع سلسلة نصية (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 العديد من الأنواع المساعدة المدمجة التي تستفيد من الأنواع المحوّلة والأنواع الشرطية. يمكن استخدام هذه الأنواع المساعدة كعناصر بناء لتحويلات أنواع أكثر تعقيدًا.

هذه الأنواع المساعدة هي أدوات قوية يمكنها تبسيط معالجة الأنواع المعقدة. على سبيل المثال، يمكنك دمج 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.

أفضل الممارسات والاعتبارات

الخاتمة

تعتبر الأنواع المحوّلة (Mapped Types) والأنواع الشرطية (Conditional Types) ميزات قوية في TypeScript تمكنك من إنشاء تحويلات أنواع مرنة ومعبرة للغاية. من خلال إتقان هذه المفاهيم، يمكنك تحسين أمان النوع وقابلية الصيانة والجودة الشاملة لتطبيقات TypeScript الخاصة بك. من التحويلات البسيطة مثل جعل الخصائص اختيارية أو للقراءة فقط إلى التحويلات التعاودية المعقدة والمنطق الشرطي، توفر هذه الميزات الأدوات التي تحتاجها لبناء تطبيقات قوية وقابلة للتطوير. استمر في استكشاف وتجربة هذه الميزات لإطلاق العنان لإمكاناتها الكاملة وتصبح مطور TypeScript أكثر كفاءة.

بينما تواصل رحلتك مع TypeScript، تذكر الاستفادة من ثروة الموارد المتاحة، بما في ذلك وثائق TypeScript الرسمية والمجتمعات عبر الإنترنت والمشاريع مفتوحة المصدر. احتضن قوة الأنواع المحوّلة والأنواع الشرطية، وستكون مجهزًا جيدًا لمواجهة حتى أكثر المشكلات المتعلقة بالأنواع تحديًا.