تخطّى إلى المحتوى الرئيسي
noUncheckedIndexedAccess وexactOptionalPropertyTypes: كيف نفعّل خيارات الصرامة الإضافية في TypeScript داخل قواعد شيفرة حقيقية

noUncheckedIndexedAccess وexactOptionalPropertyTypes: كيف نفعّل خيارات الصرامة الإضافية في TypeScript داخل قواعد شيفرة حقيقية

October 30, 2026
هندسة الواجهات
7 دقيقة قراءة

لا يجعل الخيار "strict": true لغة TypeScript تتحقق من القراءة عبر فهارس المصفوفات والسجلات، ولا من تمرير undefined صراحةً إلى الخصائص الاختيارية. يسدّ خياران إضافيان في المترجم هاتين الثغرتين، هما noUncheckedIndexedAccess وexactOptionalPropertyTypes، ويتيح خط أساس للأخطاء لكل ملف في CI اعتمادهما تدريجيًا في قاعدة شيفرة قائمة بدلًا من طلب دمج ضخم واحد.

فعّل فريقنا الخيارين في قواعد شيفرة قائمة مبنية على React وNode.js. يشرح هذا المقال ما يلتقطه كل خيار، وأنماط الإصلاح التي اعتمدناها، وسكربت "السقّاطة" (ratchet) الذي يتيح اعتمادهما ملفًا تلو الآخر. تحققنا من كل سلوك للمترجم مذكور هنا على TypeScript 5.9.3.

أهم النقاط

• الخيار noUncheckedIndexedAccess (منذ TypeScript 4.1) يجعل نوع كل قراءة عبر فهرس مصفوفة أو توقيع فهرسة T | undefined، فيصبح rows[0].id خطأ ترجمة حتى تُعالَج حالة الغياب.

• الخيار exactOptionalPropertyTypes (منذ TypeScript 4.4) يمنع الخاصية الاختيارية المعرّفة بالشكل prop?: T من قبول undefined صريحة، فيلتقط حالة الدمج { ...defaults, ...overrides } التي تستبدل قيمة افتراضية بـ undefined.

• لا يدخل أيٌّ من الخيارين ضمن "strict": true، لكن ملف tsconfig.json الذي يولّده tsc --init منذ TypeScript 5.9 يفعّل الخيارين معًا.

• نصلح القراءة عبر الفهارس بالتضييق (narrowing) وحلقات for...of والتحقق بعد التفكيك، لا بعامل التأكيد ! الذي يعيد الانهيار نفسه الذي وُجد الخيار لمنعه.

• نعتمد الخيارين عبر ملف ثانٍ باسم tsconfig.strict.json وخط أساس للأخطاء لكل ملف، يُفشل CI فقط عندما يزداد عدد أخطاء ملف ما.

ما الذي يتركه "strict": true خارج التحقق؟

الخيار "strict": true مظلّة تفعّل مجموعة ثابتة من الفحوص: strictNullChecks وnoImplicitAny وstrictFunctionTypes وstrictBindCallApply وstrictPropertyInitialization وnoImplicitThis وuseUnknownInCatchVariables وalwaysStrict وstrictBuiltinIteratorReturn. لا ينتمي noUncheckedIndexedAccess ولا exactOptionalPropertyTypes إلى هذه المجموعة، لأن تفعيلهما يكسر الكثير من الشيفرة القائمة.

في الوضع الصارم العادي تقبل TypeScript السطر const firstId: string = rows[0].id رغم أن المصفوفة rows قد تكون فارغة، وتقبل const max: number = limits['uploads'] على Record<string, number> رغم أن المفتاح قد لا يكون موجودًا. يُترجم السطران بلا أخطاء، وقد ينتج كلاهما undefined وقت التشغيل.

صارت TypeScript نفسها تدفع المشاريع الجديدة نحو إعدادات أكثر صرامة. فمنذ TypeScript 5.9 يضبط ملف tsconfig.json الذي يولّده tsc --init الخيارين noUncheckedIndexedAccess وexactOptionalPropertyTypes على true بجانب strict. أما قواعد الشيفرة الأقدم فنادرًا ما تحتويهما ما لم يضفهما أحد عن قصد.

ما الذي يلتقطه noUncheckedIndexedAccess فعلًا؟

يضيف noUncheckedIndexedAccess النوع undefined إلى كل قراءة عبر فهرس مصفوفة أو توقيع فهرسة مثل Record<string, T>. مع تفعيله يصبح نوع rows[0] هو User | undefined، ونوع limits['uploads'] هو number | undefined. أما الخصائص المعرّفة بأسمائها، ومواقع الصفوف (tuples) ضمن طولها المعروف، والقيم الناتجة عن for...of فتحتفظ بأنواعها العادية.

يفاجئ سلوكان معظم الفرق. الأول أن التحقق من الطول لا يضيّق القراءة عبر الفهرس: داخل if (rows.length > 0) يبقى rows[0] من النوع User | undefined. والثاني أن تفكيك المصفوفات مشمول أيضًا، فالسطر const [first] = rows يعطي first النوع User | undefined.

أنماط الإصلاح لدينا: نقرأ العنصر مرة واحدة في متغير ثم نضيّق هذا المتغير بشرط if، ونكرّر عبر for...of أو rows.entries() بدل الفهرسة، ونجعل عمليات البحث التي قد تفشل تبدو كذلك، فنتحقق من الدالة المستخرجة من handlers[event.type] قبل استدعائها. وفي جداول البحث التي تُبنى وقت التشغيل نستبدل غالبًا Record<string, T> بـ Map<string, T>، لأن Map.get تعيد أصلًا T | undefined. أما Record بمفاتيح من اتحاد ثابت مثل Record<'free' | 'pro', number> فليس توقيع فهرسة، لذا تبقى قراءته غير اختيارية.

نفعّل كذلك قاعدة lint تمنع تأكيدات عدم القيمة الفارغة، وهي @typescript-eslint/no-non-null-assertion أو noNonNullAssertion في Biome، لأن rows[0]!.id يُسكت الخيار ويُبقي الانهيار.

ما الخطأ الذي يمنعه exactOptionalPropertyTypes؟

يجعل exactOptionalPropertyTypes الصيغة prop?: T تعني أن الخاصية إما غائبة وإما تحمل قيمة من النوع T، ويمنعها من قبول undefined صريحة. من دون هذا الخيار تعامل TypeScript حالتي "غائبة" و"موجودة لكن قيمتها undefined" كحالة واحدة. أما JavaScript فلا تفعل ذلك: قيمة undefined صريحة داخل نشر الكائن (spread) تستبدل القيمة التي قبلها، والعامل in يعدّ المفتاح موجودًا.

المثال الكلاسيكي هو دمج القيم الافتراضية. الدالة withDefaults(overrides) تستقبل كائن Options جزئيًا، مكتوب النوع بالأداة Partial، وتعيد { ...defaults, ...overrides }. استدعاء withDefaults({ timeoutMs: undefined }) يُترجم في الوضع الصارم العادي، ونوع الإرجاع يعد بأن timeoutMs من النوع number، لكن القيمة الفعلية undefined، وهي قيمة تعاملها setTimeout كمهلة فورية. مع تفعيل exactOptionalPropertyTypes يفشل هذا الاستدعاء بالخطأ TS2379.

ينطبق التمييز نفسه على نقاط PATCH، حيث يعني غياب الحقل "اتركه كما هو" بينما تعني القيمة الصريحة "غيّره".

الاحتكاك الأكبر يظهر في خصائص مكوّنات React وفي القيم التي نوعها أصلًا T | undefined. تمرير hint={maybeHint} إلى مكوّن تعرّف خصائصه hint?: string يفشل بالخطأ TS2375. في الخصائص التي تُمرَّر كما هي نعرّف hint?: string | undefined، وهذا بالضبط ما تفعله @types/react مع خصائص DOM مثل className. وفي كائنات الإعداد لا نضيف المفتاح إلا عند وجود قيمة، عبر نشر شرطي: ...(timeoutMs !== undefined && { timeoutMs }). ولا يساعد skipLibCheck مع أنواع المكتبات الخارجية، لأن الخطأ يُبلَّغ عنه في موضع الاستدعاء.

ما خيارات الصرامة الإضافية الأخرى التي تستحق التفعيل؟

• noUncheckedIndexedAccess (TypeScript 4.1): يلتقط القراءة عبر فهارس المصفوفات وتواقيع الفهرسة التي قد لا تجد قيمة. كلفة اعتماده عالية في الشيفرة الكثيفة بالبيانات لكنها آلية في معظمها. نفعّله.

• exactOptionalPropertyTypes (TypeScript 4.4): يلتقط undefined الصريحة في الخصائص الاختيارية. كلفة متوسطة مع احتكاك في أنواع المكتبات الخارجية. نفعّله.

• noImplicitOverride (TypeScript 4.3): يفرض الكلمة المفتاحية override على الأعضاء التي تتجاوز أعضاء الصنف الأساسي، فلا تؤدي إعادة تسمية في الصنف الأساسي إلى عزل دالة في الصنف الفرعي بصمت. كلفة منخفضة. نفعّله حيثما توجد أصناف، كخدمات NestJS.

• noFallthroughCasesInSwitch (TypeScript 1.8): يلتقط كتل case غير الفارغة التي تخلو من break أو return. كلفة منخفضة. نفعّله.

• noPropertyAccessFromIndexSignature (TypeScript 4.2): يفرض الوصول بالأقواس مثل process.env['KEY'] لمفاتيح تواقيع الفهرسة. كلفته منخفضة لكنه مزعج، وnoUncheckedIndexedAccess يعطي هذه القراءات أصلًا نوعًا قد يكون undefined، لذا نتركه معطّلًا.

كيف نعتمد هذه الخيارات في قاعدة شيفرة كبيرة من دون طلب دمج ضخم واحد؟

تنطبق خيارات المترجم على البرنامج بأكمله، فلا يوجد مفتاح لكل ملف على حدة لـ noUncheckedIndexedAccess. نُبقي ملف tsconfig.json الرئيسي دون تغيير حتى تبقى المحررات وعمليات البناء الإنتاجية خضراء، ونضيف ملف tsconfig.strict.json يمتد منه ويفعّل الخيارين. ولا يتحقق من الأنواع وفق هذا الملف الثاني إلا CI.

يشغّل سكربت Node.js صغير الأمر tsc -p tsconfig.strict.json --noEmit --pretty false، ويحلّل كل سطر بالشكل file(line,col): error TSxxxx، ويعدّ الأخطاء لكل ملف، ثم يقارن الأعداد بملف ts-strict-baseline.json محفوظ في المستودع. يفشل CI فقط عندما يتجاوز عدد أخطاء ملف ما قيمته في خط الأساس. يجب أن تكون الملفات الجديدة نظيفة من اليوم الأول، وتُصلَح الملفات القديمة بالوتيرة التي يتحملها الفريق.

تشغيل السكربت مع الخيار --update يعيد كتابة خط الأساس، وكل طلب دمج يزيل أخطاء يحدّثه، حتى لا تتحرك الأعداد إلا نزولًا. وعندما يفرغ خط الأساس ينتقل الخياران إلى tsconfig.json الرئيسي ويُحذف السكربت. نتجنب البديل الشائع، وهو إدراج تعليقات // @ts-expect-error بالجملة، لأن كل تعليق يُخفي كل خطأ في السطر الذي يليه، ونادرًا ما تُزال هذه التعليقات لاحقًا.

لماذا تزداد أهمية هذه الخيارات مع الشيفرة المولّدة بالذكاء الاصطناعي؟

تكتب مساعدات البرمجة بالذكاء الاصطناعي المسار السعيد بطلاقة، وشيفرة مثل results[0].message أو config[key].enabled هي بالضبط افتراض المسار السعيد الذي يرفضه هذان الخياران. عندما يشغّل وكيل مثل Claude Code الأمر tsc --noEmit بعد كل تعديل، يحوّل الإعداد الأكثر صرامة افتراضًا خفيًا وقت التشغيل إلى خطأ محدد بملف وسطر، يستطيع الوكيل إصلاحه في الدورة نفسها قبل أن يراجع إنسان التغييرات.

في Devya نعامل الخيارين كحواجز حماية رخيصة. كلفتهما إصلاحات آلية في البداية، ثم يرفضان فئة كاملة من انهيارات "Cannot read properties of undefined" في كل تغيير لاحق، أيًّا كان كاتبه، إنسانًا أو أداة.

الأسئلة الشائعة

س: هل يدخل noUncheckedIndexedAccess ضمن "strict": true؟

ج: لا. لا يفعّل "strict": true أيًّا من noUncheckedIndexedAccess وexactOptionalPropertyTypes، ويجب ضبط كل منهما صراحةً في compilerOptions. ومنذ TypeScript 5.9 يضيف tsc --init الخيارين إلى ملفات الإعداد الجديدة.

س: هل يؤثر noUncheckedIndexedAccess في حلقات for...of أو في الصفوف (tuples)؟

ج: لا. تحتفظ القيم الناتجة عن for...of وArray.prototype.entries() بنوع العنصر، وتبقى مواقع الصفوف ضمن طولها المعروف غير اختيارية. وحدها القراءة عبر فهرس مصفوفة أو توقيع فهرسة تكتسب | undefined.

س: لماذا لا يضيّق الشرط if (arr.length > 0) نوع arr[0]؟

ج: لا تربط TypeScript طول المصفوفة بالقراءات الفردية عبر الفهارس، فيبقى arr[0] من النوع T | undefined بعد التحقق من الطول. اقرأ العنصر في متغير ثم تحقق من هذا المتغير.

س: كيف نصلح رسالة "Consider adding 'undefined' to the types of the target's properties"؟

ج: تأتي هذه الرسالة (TS2375 أو TS2379) من exactOptionalPropertyTypes. عرّف الخاصية بالشكل prop?: T | undefined عندما تكون undefined مدخلًا مشروعًا، أو أضف المفتاح فقط عندما تكون القيمة معرّفة باستخدام نشر شرطي.