الوحدة 1 · الأنواع أداةً للتصميم
الـ Literal Types و as const و satisfies
18 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
امنع القيم الحرفية من الاتساع إلى string، واشتقّ أنواع union من مصفوفات وقت التشغيل باستخدام as const، وتحقّق من كائنات الإعداد بـ satisfies دون أن تفقد أنواعها الدقيقة.
- 1لماذا تتّسع القيم الحرفية
- 2as const: جمّد القيم الحرفية
- 3التعليق النوعي مقابل satisfies
- 4الاختيار بينها
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
إليك بلاغ خطأ قدّمه كل فريق TypeScript مرة على الأقل: “أمرّر النص الصحيح بالضبط ويقول لي Type 'string' is not assignable to type 'OrderStatus'.”
type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';
type Order = { id: number; status: OrderStatus };
function save(order: Order) {}
const draft = { id: 1001, status: 'processing' };
save(draft);
// Error: Argument of type '{ id: number; status: string; }' is not assignable
// to parameter of type 'Order'.النص هو 'processing' فعلًا. لكن TypeScript لم يستنتج له 'processing'، بل استنتج string. هذا هو الاتساع (widening)، وفهمه يشرح ثلاث أدوات ستستخدمها كل أسبوع: الـ literal types (الأنواع الحرفية)، وas const، وsatisfies.
لماذا تتّسع القيم الحرفية
يستنتج TypeScript النوع الذي يُرجَّح أن تحمله القيمة طوال حياتها، لا القيمة التي تبدأ بها فقط. الـ const لا يمكن أن يتغيّر أبدًا، فيحتفظ بالقيمة الحرفية. أما let أو خاصية الكائن فيمكن إعادة الإسناد إليهما، فيوسّعهما TypeScript:
const a = 'processing'; // 'processing'
let b = 'processing'; // string
const c = { status: 'processing' }; // { status: string }الأخيرة هي المفاجأة. const c تمنعك من إعادة الإسناد إلى c، لكن c.status = 'anything' ما زالت JavaScript مشروعة، فتتّسع الخاصية إلى string.
هناك ثلاثة إصلاحات جيدة، مرتّبة حسب الأفضلية. اكتب القيمة الحرفية حيث يكون النوع معروفًا أصلًا (save({ id: 1001, status: 'processing' }))، فتُفحص مقابل Order مباشرة. أو علّق المتغيّر بنوعه (const draft: Order = …) حين تبنيه مسبقًا. أو استخدم as const، وهي موضوعنا التالي.
as const: جمّد القيم الحرفية
as const تطلب من TypeScript أن يستنتج أضيق نوع ممكن: كل قيمة حرفية تبقى حرفية، وكل مصفوفة تصبح tuple للقراءة فقط، وكل خاصية تصبح readonly:
const COUNTRIES = ['EG', 'AE', 'SA', 'JO', 'GB', 'DE', 'US', 'CA'] as const;
// readonly ['EG', 'AE', 'SA', 'JO', 'GB', 'DE', 'US', 'CA']
type Country = (typeof COUNTRIES)[number];
// 'EG' | 'AE' | 'SA' | 'JO' | 'GB' | 'DE' | 'US' | 'CA'ذلك السطر الثاني من أنفع الأنماط في TypeScript اليومية. typeof COUNTRIES تحوّل القيمة إلى نوعها، و[number] تسأل “ما النوع الذي أحصل عليه حين أفهرس هذا بأي رقم؟” (ستتعرّف على هذين العاملين (operators) كما ينبغي في keyof و typeof وأنواع الوصول بالفهرس). والنتيجة union مشتقّ من مصفوفة وقت التشغيل. تحصل على مصدر حقيقة واحد: المصفوفة موجودة وقت التشغيل للقوائم المنسدلة والتحقّق، والنوع يتبعها تلقائيًا.
function isCountry(value: string): value is Country {
return (COUNTRIES as readonly string[]).includes(value);
}التحويل الموسِّع داخل isCountry ضروري لأن includes على tuple من القيم الحرفية لا تقبل إلا تلك القيم، وهذا يُفسد الغاية من فحص نص اعتباطي.
التعليق النوعي مقابل satisfies
كوبونات Cartwheel موجودة في كائن إعداد. الطريقة البديهية لتحديد نوعه هي التعليق النوعي:
type Coupon = { kind: 'percent'; percentOff: number } | { kind: 'free-shipping' };
const coupons: Record<string, Coupon> = {
WELCOME10: { kind: 'percent', percentOff: 10 },
FREESHIP: { kind: 'free-shipping' },
};
coupons.WELCOME10.percentOff; // Error: Property 'percentOff' does not exist on type 'Coupon'.
coupons.SPRNG15; // compiles, undefined at runtimeالتعليق النوعي يستبدل ما كان TypeScript يعرفه. كتبت WELCOME10 ككوبون نسبة مئوية، لكن نوعه صار مجرّد Coupon، والكائن يقبل أي مفتاح على الإطلاق، بما فيها الأخطاء الإملائية.
أما satisfies فتتحقّق من القيمة مقابل نوع دون أن تستبدل النوع المستنتَج:
const coupons = {
WELCOME10: { kind: 'percent', percentOff: 10 },
FREESHIP: { kind: 'free-shipping' },
} satisfies Record<string, Coupon>;
coupons.WELCOME10.percentOff; // number
coupons.SPRNG15; // Error: Property 'SPRNG15' does not exist on type …ما زلت تحصل على التحقّق: percentof المكتوبة خطأً أو kind: 'bogo' تُعلَّم مباشرة في الكائن الحرفي. وتحتفظ بالمفاتيح والأشكال الدقيقة. كما أن satisfies تحدّد نوع القيم الحرفية من السياق، فيبقى kind بقيمة 'percent' بدل أن يتّسع إلى string، دون الحاجة إلى as const.
استخدم التركيبة as const satisfies T حين تريد الأمرين: قيمًا حرفية للقراءة فقط بعمق، وفحصًا مقابل شكل. تُقرأ من اليسار إلى اليمين هكذا: “جمّد هذا، ثم تأكّد أنه مناسب”.
الاختيار بينها
قاعدة افتراضية بسيطة: علّق معاملات الدوال وأنواع الإرجاع والمتغيّرات التي يجب أن يكون نوعها هو النوع العام (let current: Order). استخدم satisfies لكائنات الإعداد وجداول البحث وكل ما تهمّ فيه المفاتيح والقيم المحدّدة لاحقًا. واستخدم as const للقوائم الثابتة التي تريدها union. الثلاثة كلها وقت الترجمة فقط؛ لا يغيّر أيٌّ منها بايتًا واحدًا من JavaScript الناتجة.
في التمرين ستشتقّ Country من قائمة دول Cartwheel، وتحدّد نوع جدول العملات بحيث يرفض الدول المجهولة ويتذكّر العملات بدقة. بهذا تكتمل أدوات التصميم في هذا القسم؛ وبعده تنتقل إلى الـ generics، بدءًا بالعلاقات التي تعبّر عنها.
أهم النقاط
- متغيّرات
letوخصائص الكائنات توسّع القيم الحرفية إلىstringأوnumber؛ أما متغيّراتconstفتحتفظ بالقيمة الحرفية. as constتجعل القيمة للقراءة فقط بعمق وتحتفظ بكل قيمة حرفية، ما يتيح لك اشتقاق نوع union من مصفوفة وقت التشغيل.- التعليق النوعي (annotation) يستبدل النوع المستنتَج بالنوع المكتوب؛ أما
satisfiesفتتحقّق مقابل نوع وتحتفظ بالنوع المستنتَج. - تأكيدات
asتتخطّى فحوصًا مثل الخصائص الناقصة، لذا استخدمsatisfiesحين تريد تحقّقًا لا ثقة عمياء.