EduFlow
أنماط TypeScript المتقدّمة
⁦0%⁩

الوحدة 1 · الأنواع أداةً للتصميم

أنواع تستحق مكانها

12 دقراءة 3 دبيئة تجريب الكود

الدرس في لمحة

فرّق بين type يمنع خطأً حقيقيًا وtype يكتفي بتزيين الكود، واقرأ اختبارات الأنواع التي يستخدمها كل تمرين في هذه الدورة.

  1. 1طريقتان لتحديد نوع الشيء نفسه
  2. 2المثال الذي يرافقنا
  3. 3كيف تختبر التمارين الأنواع

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

إليك خطأً وصل إلى الإنتاج في نظام دفع عملت عليه. كانت هناك دالة استرداد تأخذ amount، ومزوّد الدفع يتوقّع المبلغ بالسنتات. أحد المستدعين مرّر المبلغ بالدولارات. فاسترد العملاء 1% فقط مما يستحقّونه، ولم ينتبه أحد لأسبوعين، لأن كل سطر في ذلك الكود كان “محدَّد النوع”: amount: number.

الأنواع كانت موجودة، لكنها لم تستحق مكانها. number وصف الكود ولم يمنع شيئًا. هذه الدورة عن النوع الآخر من الـ types (الأنواع): النوع الذي يجعل كتابة الخطأ مستحيلة.

طريقتان لتحديد نوع الشيء نفسه

في Cartwheel، المتجر الذي ستعمل عليه طوال الدورة، خمس حالات للطلب: processing وshipped وdelivered وcancelled وreturned. هذه هي النسخة التزيينية:

TypeScript
type Order = { id: number; status: string };

function canCancel(order: Order): boolean {
  return order.status === 'procesing'; // typo: always false
}

هذا يمرّ في الترجمة. status من نوع string، فمقارنته بأي نص أمر مشروع. والآن النسخة التي تستحق مكانها:

TypeScript
type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';
type Order = { id: number; status: OrderStatus };

function canCancel(order: Order): boolean {
  return order.status === 'procesing';
  // Error: This comparison appears to be unintentional because the types
  // 'OrderStatus' and '"procesing"' have no overlap.
}

كود التشغيل نفسه، وسطر واحد تغيّر في الأنواع، فاختفت فئة كاملة من الأخطاء. سترى هذه الحركة مرارًا: استبدل نوعًا واسعًا (string أو number أو object) بنوع ضيّق يقول ما هو مسموح فعلًا.

المثال الذي يرافقنا

درسًا بعد درس ستبني طبقة الأنواع لعميل الـ API الخاص بالدفع في Cartwheel: الطلبات وحالاتها، ومبالغ مالية ترفض خلط العملات، والكوبونات، وعميل fetch تُحدَّد أنواع استجاباته من خريطة المسارات، والأحداث التي يطلقها الدفع، ومحلّلات تحوّل JSON غير موثوق إلى types موثوقة. يبقى كود التشغيل صغيرًا عن قصد؛ الجزء المثير هو ما يرفض الـ compiler (المترجم) قبوله.

تسير الدورة في خمس خطوات. أولًا تستخدم الأنواع أداةً للتصميم: الـ structural typing، والـ narrowing، والـ discriminated unions، وsatisfies. ثم الـ generics مع تركيز على الـ inference. ثم البرمجة على مستوى الأنواع، مع نصيحة صادقة عن متى تتوقّف. ثم أنماط تحتاجها قواعد الكود الحقيقية: الـ branded types، وأنواع Result، والتحليل عند الحدود، والأحداث المحدَّدة الأنواع. وأخيرًا الوصول إلى الإنتاج: خيارات الـ compiler، وملفات التصريحات، والـ decorators، وترحيل JavaScript.

كيف تختبر التمارين الأنواع

معظم التمارين على مستوى الأنواع. تشغّل بيئة التجربة TypeScript 5.9 في الوضع الصارم وتعطيك أداتين مساعدتين:

TypeScript
type T1 = Expect<Equal<OrderStatus, 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned'>>;

Equal<A, B> يساوي true فقط حين يتطابق النوعان، وExpect لا يقبل إلا true. إذن هذا السطر اختبار وحدة: يمرّ في الترجمة حين يكون الـ type صحيحًا ويفشل حين لا يكون.

الأداة الثانية مدمجة في TypeScript. تعليق // @ts-expect-error يقول “السطر التالي يجب أن يكون خطأً”:

TypeScript
// @ts-expect-error: 'canceled' is not a status
canCancel({ id: 1001, status: 'canceled' });

إذا مرّ السطر الذي تحته بلا خطأ، يبلّغ TypeScript عن توجيه غير مستخدَم. هكذا يتحوّل “هذا الاستدعاء الخاطئ مرفوض” إلى اختبار، وهذا مهم بقدر “هذا الاستدعاء الصحيح مقبول”. فالـ type الذي يقبل كل شيء ينجح في كل اختبار إيجابي.

في التمرين أدناه، حالات الطلب من نوع string. ضيّق الـ type وشاهد الـ compiler يجد التسمية الناقصة بدلًا منك. الدرس التالي ينظر في معنى “قابل للإسناد” (assignable) فعلًا في TypeScript، لأنه ليس ما يتوقّعه معظم القادمين من Java أو C#.

أهم النقاط

  • يستحق الـ type مكانه حين يجعل كتابة خطأ حقيقي مستحيلة، لا حين يكرّر ما يقوله الكود أصلًا.
  • استخدام string حيث لا تصحّ إلا خمس قيم فرصةٌ ضائعة؛ أما الـ union من القيم الحرفية فيسمح للـ compiler بفحص كل استخدام.
  • Expect<Equal<A, B>> يفشل في الترجمة ما لم يكن النوعان متطابقين تمامًا، لذا يعمل كاختبار وحدة للأنواع.
  • سطر // @ts-expect-error يؤكّد أن السطر التالي مرفوض، ويصبح هو نفسه خطأً إذا مرّ ذلك السطر بلا خطأ.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

أيّ هذه الأنواع يلتقط أكبر عدد من الأخطاء الحقيقية لحقل status في طلب لا يمكن أن يكون إلا processing أو shipped أو delivered أو cancelled أو returned؟

السؤال 2 من 3

ماذا يفعل اختبار النوع هذا حين يكون Total هو number | undefined؟

TypeScript
type T1 = Expect<Equal<Total, number>>;
السؤال 3 من 3

تضيف // @ts-expect-error فوق pay(order, 'EUR')، لكن pay تقبل أي نص كعملة. ماذا يحدث؟

تدريب

بيئة تجريب الكود

ضيّق نوع حالة الطلب

لطلبات Cartwheel خمس حالات بالضبط: processing وshipped وdelivered وcancelled وreturned. استبدل string في OrderStatus بـ union من هذه القيم الحرفية الخمس حتى تنجح اختبارات الأنواع. وحين يضيق النوع، يشير الـ compiler إلى فجوة في statusLabels: املأها حتى تُعيد statusLabel('returned') القيمة 'Returned'.

// Cartwheel's five order statuses come from the orders table.
// Right now the type accepts any string at all.
type OrderStatus = string;

const statusLabels: Record<OrderStatus, string> = {
  processing: 'Processing',
  shipped: 'Shipped',
  delivered: 'Delivered',
  cancelled: 'Cancelled',
};

function statusLabel(status: OrderStatus): string {
  return statusLabels[status];
}

function canCancel(status: OrderStatus): boolean {
  return status === 'processing';
}

// ---- Type tests (do not edit) ----
type T1 = Expect<Equal<OrderStatus, 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned'>>;

// @ts-expect-error: 'canceled' (one L) is a typo, not a status
canCancel('canceled');

console.log(statusLabel('shipped'), canCancel('processing'));
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • الملف يجتاز فحص الأنواع بلا أي خطأ (بما فيها اختبارات الأنواع)
  • statusLabel('returned') تُعيد 'Returned'
  • canCancel ما زالت لا تسمح بإلغاء إلا الطلبات في حالة processing