الوحدة 1 · الأنواع أداةً للتصميم
أنواع تستحق مكانها
12 دقراءة 3 دبيئة تجريب الكود
الدرس في لمحة
فرّق بين type يمنع خطأً حقيقيًا وtype يكتفي بتزيين الكود، واقرأ اختبارات الأنواع التي يستخدمها كل تمرين في هذه الدورة.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
إليك خطأً وصل إلى الإنتاج في نظام دفع عملت عليه. كانت هناك دالة استرداد تأخذ amount، ومزوّد الدفع يتوقّع المبلغ بالسنتات. أحد المستدعين مرّر المبلغ بالدولارات. فاسترد العملاء 1% فقط مما يستحقّونه، ولم ينتبه أحد لأسبوعين، لأن كل سطر في ذلك الكود كان “محدَّد النوع”: amount: number.
الأنواع كانت موجودة، لكنها لم تستحق مكانها. number وصف الكود ولم يمنع شيئًا. هذه الدورة عن النوع الآخر من الـ types (الأنواع): النوع الذي يجعل كتابة الخطأ مستحيلة.
طريقتان لتحديد نوع الشيء نفسه
في Cartwheel، المتجر الذي ستعمل عليه طوال الدورة، خمس حالات للطلب: processing وshipped وdelivered وcancelled وreturned. هذه هي النسخة التزيينية:
type Order = { id: number; status: string };
function canCancel(order: Order): boolean {
return order.status === 'procesing'; // typo: always false
}هذا يمرّ في الترجمة. status من نوع string، فمقارنته بأي نص أمر مشروع. والآن النسخة التي تستحق مكانها:
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 في الوضع الصارم وتعطيك أداتين مساعدتين:
type T1 = Expect<Equal<OrderStatus, 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned'>>;Equal<A, B> يساوي true فقط حين يتطابق النوعان، وExpect لا يقبل إلا true. إذن هذا السطر اختبار وحدة: يمرّ في الترجمة حين يكون الـ type صحيحًا ويفشل حين لا يكون.
الأداة الثانية مدمجة في TypeScript. تعليق // @ts-expect-error يقول “السطر التالي يجب أن يكون خطأً”:
// @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يؤكّد أن السطر التالي مرفوض، ويصبح هو نفسه خطأً إذا مرّ ذلك السطر بلا خطأ.