تضييق الأنواع (type narrowing) هو أن تحدد TypeScript نوعًا أدق لقيمة في نقطة معينة من الكود، بناءً على الفحوص التي أجراها الكود مسبقًا. يصبح معامل string | number من نوع string داخل if (typeof x === "string") ومن نوع number في else.
استدعاء value.toFixed(2) قبل الفحص كان سيكون خطأ ترجمة، لأن toFixed غير موجودة في string. الفحص JavaScript عادية تعمل وقت التشغيل؛ والتضييق هو قراءة المترجم له وتعديل النوع. لا يُنتج أي شيء إضافي.
تحليل مسار التحكم
تتبع TypeScript كل مسار داخل الدالة: if/else، وreturn وthrow المبكرتين، وswitch، والحلقات، والعوامل المختصرة && و|| و?? و?:. في كل نقطة يكون نوع المتغير هو ما يظل ممكنًا هناك.
أسلوب الإعادة المبكرة («guard clauses») هو أوضح طريقة للتضييق: عالج الحالات الشاذة أولًا فيعمل بقية الدالة بالنوع النظيف.
كل طرق التضييق
| الصيغة | مثال | تضيّق |
|---|---|---|
| typeof | typeof x === "string" | الأنواع البدائية والدوال |
| القيمة المنطقية | if (x) | تحذف null وundefined والقيم الحرفية الخاطئة منطقيًا |
| المساواة | x === "a" وx == null وx !== undefined | القيم الحرفية وnull وundefined |
in | "swim" in pet | unions الكائنات، بحسب الخاصية |
| instanceof | err instanceof TypeError | نسخ الأصناف |
Array.isArray | Array.isArray(x) | المصفوفات مقابل كل ما عداها |
| الإسناد | x = 5 | إلى النوع المُسند |
| Type predicate | function isUser(x: unknown): x is User | أي شيء يمكنك فحصه |
| دالة تأكيد | function assertUser(x: unknown): asserts x is User | كل ما بعد الاستدعاء |
| خاصية مميّزة | switch (shape.kind) | الـ unions الموسومة |
الثلاثة الأخيرة مشروحة في صفحتي type guards وdiscriminated unions. والباقي أدناه.
التضييق بالقيمة المنطقية
يحذف if (x) النوعين null وundefined (والأنواع الحرفية false و0 و""). إنه قصير، وفيه فخ كلاسيكي واحد: 0 و"" قيمتان خاطئتان منطقيًا، فتُعامل قيم صحيحة كأنها غائبة.
للأرقام والنصوص، قارن مع undefined أو null صراحة (أو استخدم ??). والفحص بالقيمة المنطقية مناسب للكائنات والمصفوفات والدوال، التي لا تكون خاطئة منطقيًا أبدًا.
التضييق بالمساواة
تضيّق === و!== و== و!= الطرفين. المقارنة مع قيمة حرفية تضيّق إلى تلك القيمة؛ و== null (المساواة المرنة) يطابق null وundefined في فحص واحد، وهو الموضع الوحيد الذي تكون فيه المساواة المرنة أسلوبًا مألوفًا.
مقارنة متغيرين تضيّق كليهما إلى ما قد يشتركان فيه: إذا اجتاز a: string | number وb: string | boolean الفحص a === b، يكون كلاهما string داخل if.
العامل in
يضيّق "key" in obj union من أنواع الكائنات إلى الأعضاء التي تملك (أو قد تملك) تلك الخاصية.
يعمل in أيضًا مع unknown بمجرد أن تعرف أنه كائن: بعد typeof v === "object" && v !== null && "id" in v تعرف TypeScript أن v يملك خاصية id نوعها unknown. وللـ unions التي تصممها بنفسك، تكون خاصية وسم مشتركة (kind: "fish") أوضح من البحث عن الدوال: يُسمى هذا النمط discriminated union.
التضييق بالإسناد
للمتغير نوع معلن، ونوع مضيَّق يتبع إسناداته. إسناد قيمة يضيّقه إلى نوع تلك القيمة، في حدود النوع المعلن.
أين يضيع التضييق
التضييق محلي ومتحفظ. بضع حالات تعيد ضبطه:
- تعبير مختلف. فحص
obj.nameيضيّقobj.name(وobj["name"])، لكن لا يضيّقobj[key]عندما يكونkeyمتغيرًا من نوعstringلا قيمة حرفية، ولا نسخة أُنشئت قبل الفحص. - الـ callbacks وإعادة الإسناد. داخل callback، يحتفظ
letالمضيَّق بتضييقه فقط إذا لم يُسند مرة أخرى بعد إنشاء الـ callback. أماconstأو المعامل الذي لا يُعاد إسناده فيبقى مضيَّقًا. - الفحوص المخفية في الدوال المساعدة. دالة
isString(x: unknown): booleanلا تخبر المترجم بشيء. أعطها نوع قيمة مُعادة هو type predicate،x is string، فتضيّق استدعاءاتها كما يفعلtypeof.
يبلّغ المترجم index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. قد يعمل الـ callback لاحقًا، بعد x = undefined. احذف ذلك الإسناد الأخير (أو انسخ القيمة إلى const داخل if) فيُترجم ويطبع 5 مرتين.
يمكن إصلاح الدالة المساعدة التي تعيد boolean بإعلان ما تثبته. هذا هو type guard:
function isString(value: unknown): value is string {
return typeof value === "string";
}
منذ TypeScript 5.5 يستنتج المترجم مثل هذه الـ predicates للدوال السهمية البسيطة، ولهذا يعيد list.filter((x) => x !== undefined) الآن مصفوفة دون undefined.
الأسئلة الشائعة
ما هو تضييق الأنواع في TypeScript؟
التضييق هو أن تحدد TypeScript نوع متغير بدقة أكبر داخل كتلة بناءً على فحص يجريه الكود. بعد if (typeof x === "string") يكون string | number مجرد string داخل if ومجرد number في else. يتتبع المترجم if وelse وreturn وswitch و&& و|| و?: ليحدد النوع في كل نقطة، ويُسمى هذا تحليل مسار التحكم (control flow analysis).
لماذا لا تضيّق TypeScript النوع لدي؟
أسباب شائعة: الفحص على تعبير مختلف عن الذي تستخدمه (فُحص obj.a واستُخدم obj[key] مع key من نوع string)؛ أو القيمة let يُعاد إسنادها بعد إنشاء callback، فيفقد الـ callback التضييق؛ أو الفحص مخفي في دالة مساعدة تعيد boolean عاديًا بدل type predicate بصيغة x is T.
هل يعمل تضييق الأنواع وقت التشغيل؟
الفحوص تعمل: typeof وinstanceof وin و=== JavaScript عادية تُنفذ. أما التضييق نفسه فوقت الترجمة فقط. تقرأ TypeScript فحوصك وقت التشغيل وتعدّل الأنواع الثابتة لتطابقها، ولا يُضاف شيء إلى كود JavaScript الناتج.
كيف أضيّق النوع unknown في TypeScript؟
بالفحوص نفسها: typeof value === "string" وArray.isArray(value) وvalue instanceof Date، أو للكائنات typeof value === "object" && value !== null && "id" in value. وللفحوص القابلة لإعادة الاستخدام اكتب دالة type guard نوع قيمتها المُعادة value is T.
كيف أحذف undefined من مصفوفة في TypeScript؟
يعيد items.filter((x) => x !== undefined) النوع T[] دون undefined منذ TypeScript 5.5، الذي يستنتج الدالة الممررة كـ type predicate. وفي الإصدارات الأقدم اكتب الـ predicate بنفسك: items.filter((x): x is T => x !== undefined).