دليل شامل لأنواع TypeScript المتقدمة: أمثلة وتطبيقات عملية

دقائق القراءة: 13

مقدمة إلى عالم TypeScript المتقدم

تُعد لغة TypeScript إضافة قوية لعالم تطوير الويب، حيث توفر نظامًا صارمًا للأنواع (Type System) يتيح للمطورين تحديد أنواع المتغيرات، ومعاملات الدوال، والقيم المُرجعة، وخصائص الكائنات بدقة. هذا النظام لا يساهم فقط في اكتشاف الأخطاء مبكرًا أثناء مرحلة التطوير، بل يعزز أيضًا قابلية قراءة الكود وصيانته. في هذا الدليل الشامل، سنغوص في أعماق أنواع TypeScript المتقدمة، مقدمين أمثلة عملية وشروحات وافية لمساعدتك على إتقانها واستخدامها بفعالية في مشاريعك.

سنتناول في هذا الدليل الأنواع التالية:

أنواع التقاطع (Intersection Types): دمج الخصائص بذكاء

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


type LeftType = {
  id: number;
  left: string;
}

type RightType = {
  id: number;
  right: string;
}

type IntersectionType = LeftType & RightType;

function showType ( args: IntersectionType ) {
  console.log(args)
}

showType({ id: 1, left: "test", right: "test" });
// Output: {id: 1, left: "test", right: "test"}

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

أنواع الاتحاد (Union Types): مرونة في تحديد الأنواع

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


type UnionType = string | number;

function showType ( arg: UnionType ) {
  console.log(arg)
}

showType("test"); // Output: test
showType(7);      // Output: 7

في المثال أعلاه، الدالة showType تقبل معاملًا من نوع اتحاد UnionType، مما يعني أنها تستطيع استقبال قيم نصية (string) أو رقمية (number) كمعامل لها. يُستخدم الرمز | للفصل بين الأنواع الممكنة.

الأنواع العامة (Generic Types): قابلية إعادة الاستخدام والكفاءة

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


function showType < T >( args: T ) {
  console.log(args)
}

showType("test"); // Output: "test"
showType(1);      // Output: 1

لإنشاء نوع عام، تحتاج إلى استخدام الأقواس المعقوفة (<>) وتمرير T كمعامل (الاسم T هو مجرد اصطلاح ويمكنك استخدام أي اسم آخر). هنا، استخدمنا T، ثم استدعينا الدالة showType مرتين بأنواع مختلفة لأنها عامة ويمكن إعادة استخدامها.


interface GenericType<T> {
  id: number;
  name: T;
}

function showType ( args: GenericType< string > ) {
  console.log(args)
}

showType({ id: 1, name: "test" }); // Output: {id: 1, name: "test"}

function showTypeTwo ( args: GenericType< number > ) {
  console.log(args)
}

showTypeTwo({ id: 1, name: 4 }); // Output: {id: 1, name: 4}

هنا، لدينا مثال آخر يستخدم واجهة GenericType التي تستقبل نوعًا عامًا T. وبما أنها قابلة لإعادة الاستخدام، يمكننا استدعاؤها أولاً بنوع نصي (string) ثم بنوع رقمي (number).


interface GenericType<T, U> {
  id: T;
  name: U;
}

function showType ( args: GenericType< number , string > ) {
  console.log(args)
}

showType({ id: 1, name: "test" }); // Output: {id: 1, name: "test"}

function showTypeTwo ( args: GenericType< string , string []> ) {
  console.log(args)
}

showTypeTwo({ id: "001", name: [ "This", "is", "a", "Test" ] });
// Output: {id: "001", name: Array["This", "is", "a", "Test"]}

يمكن للنوع العام أن يستقبل عدة معاملات. هنا، قمنا بتمرير معاملين: T و U، ثم استخدمناهما كتعليقات للأنواع للخصائص. بناءً على ذلك، يمكننا الآن استخدام الواجهة وتقديم أنواع مختلفة كمعاملات.

أنواع الأدوات المساعدة (Utility Types): تبسيط التعامل مع الأنواع

توفر TypeScript مجموعة من الأدوات المساعدة المدمجة التي تسهل التعامل مع الأنواع ومعالجتها بفعالية. لاستخدامها، تحتاج إلى تمرير النوع الذي ترغب في تحويله داخل الأقواس المعقوفة <>.

Partial<T>: جعل الخصائص اختيارية

يسمح لك Partial<T> بجعل جميع خصائص النوع T اختيارية. سيضيف علامة استفهام ? بجانب كل حقل، مما يعني أنه يمكن حذف هذه الخصائص عند إنشاء كائن من هذا النوع.


interface PartialType {
  id: number;
  firstName: string;
  lastName: string;
}

function showType ( args: Partial<PartialType> ) {
  console.log(args)
}

showType({ id: 1 }); // Output: {id: 1}
showType({ firstName: "John", lastName: "Doe" }); // Output: {firstName: "John", lastName: "Doe"}

كما ترى، لدينا واجهة PartialType تُستخدم كتعليق نوع للمعاملات التي تستقبلها الدالة showType(). ولجعل الخصائص اختيارية، يجب علينا استخدام الكلمة المفتاحية Partial وتمرير النوع PartialType كمعامل. بناءً على ذلك، أصبحت جميع الحقول اختيارية.

Required<T>: فرض وجود جميع الخصائص

على عكس Partial، تقوم أداة Required بجعل جميع خصائص النوع T إلزامية، حتى لو تم تعريفها في الأصل كاختيارية. هذا يضمن أن الكائنات من هذا النوع تحتوي على جميع الخصائص المحددة.


interface RequiredType {
  id: number;
  firstName?: string;
  lastName?: string;
}

function showType ( args: Required<RequiredType> ) {
  console.log(args)
}

showType({ id: 1, firstName: "John", lastName: "Doe" });
// Output: { id: 1, firstName: "John", lastName: "Doe" }

// showType({ id: 1 }); // Error: Type '{ id: number: }' is missing the following properties from type 'Required<RequiredType>': firstName, lastName

ستجعل أداة Required جميع الخصائص إلزامية حتى لو جعلناها اختيارية في البداية قبل استخدام الأداة. وإذا تم حذف خاصية، فستقوم TypeScript بإصدار خطأ.

Readonly<T>: جعل الخصائص للقراءة فقط

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


interface ReadonlyType {
  id: number;
  name: string;
}

function showType ( args: Readonly<ReadonlyType> ) {
  // args.id = 4; // Error: Cannot assign to 'id' because it is a read-only property.
  console.log(args)
}

showType({ id: 1, name: "Doe" });
// Output: { id: 1, name: "Doe" }

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


interface ReadonlyType {
  readonly id: number;
  name: string;
}

Pick<T, K>: اختيار خصائص محددة

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


interface PickType {
  id: number;
  firstName: string;
  lastName: string;
}

function showType ( args: Pick<PickType, "firstName" | "lastName"> ) {
  console.log(args)
}

showType({ firstName: "John", lastName: "Doe" }); // Output: {firstName: "John", lastName: "Doe"}
// showType({ id: 3 }); // Error: Object literal may only specify known properties, and 'id' does not exist in type 'Pick<PickType, "firstName" | "lastName">'

تختلف Pick قليلاً عن الأدوات المساعدة السابقة التي رأيناها. فهي تتوقع معاملين: T هو النوع الذي تريد اختيار العناصر منه، و K هي الخاصية (أو الخصائص) التي تريد تحديدها. يمكنك أيضًا اختيار حقول متعددة عن طريق فصلها برمز الأنبوب (|).

Omit<T, K>: استبعاد خصائص غير مرغوبة

تُعد أداة Omit عكس نوع Pick. وبدلاً من اختيار العناصر، ستقوم بإزالة الخصائص K من النوع T. هذا مفيد لإنشاء أنواع جديدة تستبعد خصائص معينة من نوع موجود.


interface PickType {
  id: number;
  firstName: string;
  lastName: string;
}

function showType ( args: Omit<PickType, "firstName" | "lastName"> ) {
  console.log(args)
}

showType({ id: 7 }); // Output: {id: 7}
// showType({ firstName: "John" }); // Error: Object literal may only specify known properties, and 'firstName' does not exist in type 'Pick<PickType, "id">'

هذه الأداة المساعدة تشبه طريقة عمل Pick. فهي تتوقع النوع والخصائص التي يجب حذفها من هذا النوع.

Extract<T, U>: استخلاص الخصائص المشتركة

يسمح لك Extract بإنشاء نوع عن طريق اختيار الخصائص الموجودة في نوعين مختلفين. ستقوم الأداة باستخلاص جميع الخصائص من T التي يمكن تعيينها لـ U.


interface FirstType {
  id: number;
  firstName: string;
  lastName: string;
}

interface SecondType {
  id: number;
  address: string;
  city: string;
}

type ExtractType = Extract<keyof FirstType, keyof SecondType>;
// Output: "id"

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

Exclude<T, U>: استبعاد الخصائص المشتركة

على عكس Extract، ستقوم أداة Exclude بإنشاء نوع عن طريق استبعاد الخصائص الموجودة بالفعل في نوعين مختلفين. فهي تستبعد من T جميع الحقول التي يمكن تعيينها لـ U.


interface FirstType {
  id: number;
  firstName: string;
  lastName: string;
}

interface SecondType {
  id: number;
  address: string;
  city: string;
}

type ExcludeType = Exclude<keyof FirstType, keyof SecondType>;
// Output: "firstName" | "lastName"

كما ترى هنا، الخصائص firstName و lastName ليست موجودة في النوع SecondType. وباستخدام الكلمة المفتاحية Exclude، نحصل على هذه الحقول كما هو متوقع، لأنها ليست مشتركة.

Record<K, T>: بناء نوع من المفاتيح والقيم

تساعدك هذه الأداة في بناء نوع باستخدام مجموعة من الخصائص K من نوع معين T. تُعد Record مفيدة جدًا عندما يتعلق الأمر بربط خصائص نوع بآخر، أو لإنشاء كائنات ذات مفاتيح محددة مسبقًا.


interface EmployeeType {
  id: number;
  fullname: string;
  role: string;
}

let employees: Record< number , EmployeeType> = {
  0 : { id: 1, fullname: "John Doe", role: "Designer" },
  1 : { id: 2, fullname: "Ibrahima Fall", role: "Developer" },
  2 : { id: 3, fullname: "Sara Duckson", role: "Developer" },
}
// Output:
// 0: { id: 1, fullname: "John Doe", role: "Designer" },
// 1: { id: 2, fullname: "Ibrahima Fall", role: "Developer" },
// 2: { id: 3, fullname: "Sara Duckson", role: "Developer" }

طريقة عمل Record بسيطة نسبيًا. هنا، تتوقع number كنوع للمفاتيح، ولهذا السبب لدينا 0، 1، و 2 كمفاتيح للمتغير employees. وإذا حاولت استخدام سلسلة نصية كخاصية، فسيتم إصدار خطأ. بعد ذلك، يتم تحديد مجموعة الخصائص بواسطة EmployeeType، ومن ثم الكائن الذي يحتوي على الحقول id و fullname و role.

NonNullable<T>: إزالة القيم الفارغة

تسمح لك هذه الأداة بإزالة null و undefined من النوع T. هذا يضمن أن النوع الناتج لن يحتوي على أي قيم فارغة، مما يقلل من الأخطاء المتعلقة بالقيم غير المعرفة.


type NonNullableType = string | number | null | undefined;

function showType ( args: NonNullable<NonNullableType> ) {
  console.log(args)
}

showType("test"); // Output: "test"
showType(1);      // Output: 1
// showType( null ); // Error: Argument of type 'null' is not assignable to parameter of type 'string | number'.
// showType( undefined ); // Error: Argument of type 'undefined' is not assignable to parameter of type 'string | number'.

هنا، نمرر النوع NonNullableType كمعامل لأداة NonNullable التي تبني نوعًا جديدًا عن طريق استبعاد null و undefined من هذا النوع. بناءً على ذلك، إذا مررت قيمة فارغة، فستقوم TypeScript بإصدار خطأ. بالمناسبة، إذا أضفت العلامة --strictNullChecks إلى ملف tsconfig، فستطبق TypeScript قواعد عدم القابلية للقيم الفارغة بشكل صارم.

الأنواع المخططة (Mapped Types): تحويل خصائص الأنواع

تسمح لك الأنواع المخططة بأخذ نموذج موجود وتحويل كل خاصية من خصائصه إلى نوع جديد. لاحظ أن بعض أنواع الأدوات المساعدة التي تم تناولها سابقًا هي أيضًا أنواع مخططة.


type StringMap<T> = { [P in keyof T]: string }

function showType ( arg: StringMap<{ id: number ; name: string }> ) {
  console.log(arg)
}

// showType({ id: 1, name: "Test" }); // Error: Type 'number' is not assignable to type 'string'.
showType({ id: "testId", name: "This is a Test" }); // Output: {id: "testId", name: "This is a Test"}

سيقوم النوع StringMap<> بتحويل أي أنواع يتم تمريرها إلى نوع نصي (string). بناءً على ذلك، إذا استخدمناه في الدالة showType()، فيجب أن تكون المعاملات المستلمة من النوع النصي، وإلا فسيتم إصدار خطأ بواسطة TypeScript.

حراس الأنواع (Type Guards): التحقق من الأنواع في وقت التشغيل

تسمح لك حراس الأنواع بالتحقق من نوع متغير أو كائن باستخدام عامل تشغيل. إنها كتل شرطية تُرجع نوعًا باستخدام typeof أو instanceof أو in. هذه الآلية ضرورية للتعامل مع أنواع الاتحاد وتضييق نطاق النوع في سياقات معينة.

typeof: التحقق من الأنواع البدائية

يُستخدم عامل التشغيل typeof للتحقق من نوع البيانات البدائية (مثل string، number، boolean، symbol، undefined، bigint).


function showType ( x: number | string ) {
  if ( typeof x === "number" ) {
    return `The result is ${x + x}`
  }
  throw new Error ( `This operation can't be done on a ${ typeof x} ` )
}

// showType("I'm not a number"); // Error: This operation can't be done on a string
showType(7); // Output: The result is 14

كما ترى، لدينا كتلة شرطية عادية بلغة JavaScript تتحقق من نوع المعامل المستلم باستخدام typeof. باستخدام هذا، يمكنك الآن حماية نوعك بهذا الشرط.

instanceof: التحقق من أنواع الكائنات

يُستخدم عامل التشغيل instanceof للتحقق مما إذا كان كائن ما هو نسخة من فئة معينة.


class Foo {
  bar() {
    return "Hello World"
  }
}

class Bar {
  baz = "123"
}

function showType ( arg: Foo | Bar ) {
  if (arg instanceof Foo) {
    console.log(arg.bar());
    return arg.bar();
  }
  throw new Error ( "The type is not supported" )
}

showType( new Foo()); // Output: Hello World
// showType( new Bar()); // Error: The type is not supported

مثل المثال السابق، هذا أيضًا حارس نوع يتحقق مما إذا كان المعامل المستلم جزءًا من الفئة Foo أم لا ويتعامل معه بناءً على ذلك.

in: التحقق من وجود خاصية

يسمح لك عامل التشغيل in بالتحقق مما إذا كانت خاصية معينة (مثل x) موجودة أم لا في الكائن المستلم كمعامل.


interface FirstType {
  x: number;
}

interface SecondType {
  y: string;
}

function showType ( arg: FirstType | SecondType ) {
  if ( "x" in arg) {
    console.log( `The property ${arg.x} exists` );
    return `The property ${arg.x} exists`;
  }
  throw new Error ( "This type is not expected" );
}

showType({ x: 7 }); // Output: The property 7 exists
// showType({ y: "ccc" }); // Error: This type is not expected

يسمح لك عامل التشغيل in بالتحقق مما إذا كانت خاصية x موجودة أم لا في الكائن المستلم كمعامل.

الأنواع الشرطية (Conditional Types): اتخاذ قرارات بناءً على الأنواع

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


type NonNullable<T> = T extends null | undefined ? never : T;

هذا المثال لنوع الأداة المساعدة NonNullable يتحقق مما إذا كان النوع T هو null أو undefined أم لا، ويتعامل معه بناءً على ذلك. وكما تلاحظ، فإنه يستخدم عامل التشغيل الثلاثي (ternary operator) في JavaScript لتقرير النوع الناتج.

الخلاصة التقنية

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *