الوحدة 3 · البرمجة على مستوى الأنواع
keyof و typeof وأنواع الوصول بالفهرس
16 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
اشتقّ الأنواع من قيم وقت التشغيل ومن أنواع أخرى باستخدام typeof وkeyof والوصول بالفهرس، كي يسري التغيير في مكان واحد عبر قاعدة الكود بدل أن تتباعد النُّسخ.
- 1typeof: من القيم إلى الأنواع
- 2keyof: أسماء الخصائص
- 3الوصول بالفهرس: نوع الخاصية
- 4لماذا تُعيد Object.keys النوع string[]
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
مناطق الشحن في Cartwheel موجودة في كائن إعداد. النسخة الأولى من الكود وصفتها مرتين: مرة كبيانات، ومرة كأنواع.
const SHIPPING_ZONES = {
domestic: { countries: ['US', 'CA'], feeCents: 499 },
gulf: { countries: ['AE', 'SA'], feeCents: 1299 },
europe: { countries: ['GB', 'DE'], feeCents: 999 },
};
type Zone = 'domestic' | 'gulf' | 'europe'; // written by handبعد ستة أشهر أضاف أحدهم منطقة levant إلى الكائن ونسي النوع. لم يفشل شيء في الترجمة؛ كل ما في الأمر أن المنطقة الجديدة لم يكن ممكنًا اختيارها في أي مكان. كل نسخة مكتوبة يدويًا من حقيقةٍ ما هي نسخة قابلة للتباعد. هذا الدرس يغطّي عوامل الأنواع (type operators) الثلاثة التي تتيح لك كتابة الحقيقة مرة واحدة واشتقاق الباقي.
typeof: من القيم إلى الأنواع
في موضع النوع، تأخذ typeof قيمة وتعطيك النوع الذي استنتجه TypeScript لها. إنها ليست العامل typeof في JavaScript (الذي يُعيد نصوصًا مثل 'object' وقت التشغيل)؛ إنها لا توجد إلا وقت الترجمة.
const SHIPPING_ZONES = {
domestic: { countries: ['US', 'CA'], feeCents: 499 },
gulf: { countries: ['AE', 'SA'], feeCents: 1299 },
europe: { countries: ['GB', 'DE'], feeCents: 999 },
} as const;
type ShippingZones = typeof SHIPPING_ZONES;
// { readonly domestic: { readonly countries: readonly ['US', 'CA']; readonly feeCents: 499 }; … }as const مهمة هنا، كما كانت في الـ Literal Types و as const و satisfies. دونها سيكون countries من نوع string[] وfeeCents من نوع number، وستفقد التفاصيل الجديرة بالاشتقاق.
تعمل typeof مع الدوال أيضًا: typeof formatPrice هو التوقيع الكامل للدالة، ويمكنك تمريره إلى utility types (أنواع مساعدة) مثل ReturnType<typeof formatPrice>. ستبني ReturnType بنفسك بعد درسين.
متى يجب أن تكون القيمة هي المصدر والنوع مشتقًا، لا العكس؟ استخدم typeof حين تكون البيانات هي التعريف: جداول البحث، وإعدادات المناطق، وقوائم الخيارات التي تُعرض أيضًا في الواجهة. واكتب النوع أولًا حين يكون الشكل عقدًا مع طرف آخر، مثل حمولة API، وافحص القيم مقابله.
keyof: أسماء الخصائص
keyof T هو الـ union من أسماء خصائص T:
type Zone = keyof typeof SHIPPING_ZONES; // 'domestic' | 'gulf' | 'europe'أضف levant إلى الكائن فيتحدّث Zone من تلقاء نفسه. وهذا هو المقصود كله.
تفصيلان يستحقان المعرفة. أولًا، مع نوع فيه index signature مثل Record<string, number>، تعطي keyof النوع string (وstring | number مع { [k: string]: … }، لأن JavaScript تحوّل المفاتيح الرقمية إلى نصوص). ثانيًا، keyof على union تعطي فقط المفاتيح التي يملكها كل عضو. keyof (ShippedOrder | CancelledOrder) هو 'id' | 'status'، لأن قراءة trackingNumber من شيء قد يكون طلبًا ملغى ليست آمنة.
الوصول بالفهرس: نوع الخاصية
T[K] تبحث عن نوع الخاصية K في T، بصياغة الأقواس المربّعة نفسها المستخدمة للوصول إلى الخصائص وقت التشغيل:
type GulfZone = ShippingZones['gulf']; // { readonly countries: …; readonly feeCents: 1299 }
type GulfFee = ShippingZones['gulf']['feeCents']; // 1299
type ZoneConfig = ShippingZones[Zone]; // union of all three configs
type FeeCents = ShippingZones[Zone]['feeCents']; // 499 | 1299 | 999
type ShippingCountry = ShippingZones[Zone]['countries'][number]; // 'US' | 'CA' | 'AE' | …الفهرسة بـ union تعطي union من النتائج، فيكون ShippingZones[Zone] هو “نوع قيمة أي منطقة”. والتعبير (typeof X)[keyof typeof X] هو طريقتك للحصول على الـ union من قيم الكائن، ويتكرّر بما يكفي لأن تسمّيه قواعد كود كثيرة ValueOf<T>.
المصفوفات والـ tuples تُفهرس بـ number: T[number] هو نوع العنصر. هذا هو (typeof COUNTRIES)[number] الذي استخدمته في القسم 1، وهو أيضًا السطر الأخير أعلاه، الذي يحفر عبر مستويين من الكائنات ومصفوفة واحدة ليجمع كل دولة في كل منطقة.
الحيلة نفسها تعمل على أنواع API لم تكتبها أنت. إن أعطاك عميل مولَّد نوعًا كبيرًا اسمه CheckoutResponse، يمكنك تسمية نوع البند بـ CheckoutResponse['lines'][number] بدل نسخ حقوله.
لماذا تُعيد Object.keys النوع string[]
عاجلًا أو آجلًا ستكتب هذا وتنزعج:
for (const zone of Object.keys(SHIPPING_ZONES)) {
SHIPPING_ZONES[zone]; // Error: 'string' can't be used to index type …
}تُعيد Object.keys النوع string[]، لا Zone[]، وهذا مقصود. تذكّر التنميط البنيوي: القيمة التي نوعها { a: number } قد تكون كائنًا فيه عشر خصائص أخرى وقت التشغيل. لو وعدت Object.keys بـ (keyof T)[]، لكانت تكذب بشأن تلك العشر.
مع كائن const عرّفته بنفسك، لا سبيل لتسلّل مفاتيح إضافية إليه، يكون التحويل المحلّي معقولًا وصادقًا: Object.keys(SHIPPING_ZONES) as Zone[]. أما مع الكائنات القادمة من مكان آخر، فأبقِ string وافحص كل مفتاح.
في التمرين ستشتقّ خمسة أنواع من إعدادات الشحن في Cartwheel ومن استجابة الـ API. والدرس القادم يذهب خطوة أبعد: بدل قراءة الخصائص من نوع، ستـحوّل كل خصائص النوع دفعة واحدة.
أهم النقاط
- في موضع النوع، تعطي
typeof valueالنوع الذي استنتجه TypeScript لتلك القيمة، فتكون جسرًا من كود وقت التشغيل إلى نظام الأنواع. keyof Tهو الـ union من أسماء خصائصT، وT[K]هو نوع الخاصية (أو الخصائص) التي يسمّيهاK.(typeof CONFIG)[keyof typeof CONFIG]يعطي الـ union من أنواع قيم الكائن، وT[number]يعطي نوع عنصر المصفوفة.- تُعيد
Object.keysالنوعstring[]عن قصد: التنميط البنيوي يعني أن الكائن قد يحمل مفاتيح أكثر مما يسرده نوعه.