> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# TypeScript للمطورين: أي الأخطاء تكشفها الأنواع Types؟
- URL: https://arabroot.io/articles/typescript-للمطورين-ما-الذي-تكشفه-الانواع/
- Published: 2026-10-30T17:11:59.000Z
- Updated: 2026-10-30T19:46:08.000Z
- Description: زرعنا تسعة أخطاء في كود سلة مشتريات وفحصناها بالمترجم tsc بثلاثة إعدادات، لنرى ما يكشفه الوضع الصارم وما يمر رغم كل الأنواع ولماذا تحتاج التحقق وقت التشغيل.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, تطوير الواجهات والجوال Frontend & Mobile

لنفرض أن لديك متجراً إلكترونياً صغيراً يبيع القهوة والتمور، والواجهة مكتوبة بـ JavaScript منذ سنتين، والكود كبر من ملف واحد إلى عشرات الملفات يعمل عليها أربعة مطورين. وفي صباح أحد الأيام يصلك من قسم المبيعات طلب غريب: عميل اشترى بقيمة 90 ريالاً واستخدم كوبون خصم 10%، فدفع ريالاً واحداً. وفي نفس الأسبوع تظهر في لوحة الطلبات سلة فيها «23» قطعة بينما اختار العميل قطعتين ثم ثلاثاً، ويشتكي عميل آخر أنه أضاف تمراً نفد من المخزون إلى السلة، وتظهر لمطور جديد في الفريق رسالة `Cannot read properties of undefined` في صفحة الطلبات القديمة. وعندما تبحث عن السبب تجد في كل مرة سطراً واحداً بسيطاً: رقمان مرا إلى دالة بترتيب معكوس، وقيمة جاءت من حقل في النموذج نصاً وليست رقماً، و`await` نسيها أحدهم قبل استدعاء دالة غير متزامنة.

وهذه الأخطاء كلها من نوع واحد، وهو أن الكود افترض شيئاً عن شكل القيمة التي بين يديه، ولم يتحقق أحد من هذا الافتراض إلا المستخدم في الإنتاج. وهذا بالضبط ما جاءت TypeScript لحله، فهي تجعل هذه الافتراضات مكتوبة في الكود على شكل أنواع Types، ثم تفحصها قبل أن يعمل الكود. ولكن السؤال الذي يهمك كمطور قبل أن تقنع فريقك بنقل المشروع: أي هذه الأخطاء سوف تكشفها TypeScript فعلاً، وأيها سوف يمر رغم كل الأنواع؟ وهذا المقال من مرحلة [هندسة الواجهات وتطبيقات الويب Frontend](https://arabroot.io/start/#stage-web-frontend)، وسوف نجيب فيه على السؤال بتجربة حقيقية: نزرع تسعة أخطاء في كود سلة مشتريات، ونشغله، ثم نفحصه بالمترجم `tsc` بإعدادات مختلفة، ونرى ما الذي أمسكه وما الذي فاته.

وسوف نناقش في هذا المقال ما يلي:

- لماذا ظهرت TypeScript، وما الذي تعد به وما الذي لا تعد به بحسب أهدافها المعلنة.
- كيف يعمل المترجم `tsc` من الداخل: الفحص Type Checking والإخراج Emit، والأنواع البنيوية Structural Typing، والاستنتاج Type Inference.
- محو الأنواع Type Erasure، ولماذا لا يبقى أي نوع عندما يعمل الكود في المتصفح أو في Node.js.
- الوضع الصارم Strict Mode وخياراته، والتضييق Narrowing الذي يجعل الأنواع المتحدة Union Types مفيدة.
- التجربة العملية: تسعة أخطاء، وأربعة ملفات `tsconfig`، والمخرج سطراً بسطر، ثم الإصلاح والإخراج.
- ما لا تكشفه الأنواع: البيانات القادمة من خارج الكود، والتحقق وقت التشغيل Runtime Validation.
- نقل مشروع JavaScript قائم إلى TypeScript، وتكلفة ذلك، والأخطاء الشائعة، وقائمة تحقق.

## لماذا ظهرت TypeScript؟ JavaScript والأنواع Types

JavaScript لغة ديناميكية الأنواع Dynamically Typed، أي أن المتغير لا يحمل نوعاً ثابتاً، وإنما القيمة هي التي تحمل نوعها وقت التشغيل، فالمتغير `price` قد يحمل رقماً الآن ونصاً بعد سطرين، ولن يعترض أحد. ولهذا الأسلوب فائدة واضحة في البداية، فأنت تكتب الكود بسرعة ولا تشرح للمترجم شيئاً، ولكن كلما كبر الكود وزاد عدد المطورين صار السؤال «ما شكل هذا الكائن؟» يتكرر في كل ملف، والإجابة الوحيدة المتاحة هي أن تقرأ الكود الذي أنشأه، أو أن تشغله وترى. وأضف إلى ذلك أن JavaScript تحول القيم بين الأنواع تلقائياً Type Coercion في كثير من العمليات، فالجمع بين رقم ونص يعطي نصاً، والضرب بين نص ورقم يعطي رقماً، ولذلك فإن كثيراً من الأخطاء لا ترمي استثناء أصلاً، وإنما تعطي قيمة خاطئة تمر بهدوء إلى قاعدة البيانات أو إلى فاتورة العميل.

وفي 2012 أعلنت Microsoft عن TypeScript كلغة فوق JavaScript، أي أن كل كود JavaScript صحيح هو كود TypeScript صحيح، وتضيف عليه طبقة من الأنواع تفحص قبل التشغيل ثم تحذف. ويشرح فريق المشروع في [أهداف التصميم TypeScript Design Goals](https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals?ref=arabroot.io) ما تعد به اللغة وما لا تعد به، ومن أهم ما في هذه الصفحة أن اللغة لا تهدف إلى نظام أنواع «سليم» Sound بشكل كامل، ولا تضيف أي معلومات عن الأنواع وقت التشغيل، وأن الكود الذي تخرجه يجب أن يكون JavaScript نظيفة يمكن قراءتها. وهذان الهدفان هما مفتاح فهم كل ما سوف تراه في التجربة، فالأنواع أداة فحص قبل التشغيل، وليست حارساً يقف عند كل استدعاء دالة.

وقد يتساءل البعض: هل الفائدة كبيرة فعلاً، أم هي موضة؟ والإجابة الأقرب للأرقام جاءت من دراسة [To Type or Not to Type](https://earlbarr.com/publications/typestudy.pdf?ref=arabroot.io) المنشورة في مؤتمر ICSE عام 2017، حيث أخذ الباحثون أخطاء حقيقية أصلحت في مشاريع JavaScript مفتوحة المصدر، ورجعوا إلى الكود قبل الإصلاح، وأضافوا إليه الأنواع يدوياً، ثم سألوا: هل كان المترجم سيعترض؟ ووجدوا أن TypeScript 2.0 وFlow كشفا 15% من هذه الأخطاء. ولاحظ أن هذه أخطاء مرت من المراجعة والاختبارات ووصلت إلى المستودع العام، فالرقم الحقيقي أثناء التطوير أعلى بكل تأكيد، ولكن الرقم مهم لأنه يقول بوضوح إن 85% من الأخطاء المنشورة كانت أخطاء منطق لا تعرف عنها الأنواع شيئاً. أي أن TypeScript تقلل نوعاً محدداً من الأخطاء، ولا تغني عن الاختبارات.

ومع ذلك فقد صارت TypeScript هي الاختيار الافتراضي في كثير من الفرق، وبحسب تقرير [GitHub Octoverse 2025](https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/?ref=arabroot.io) فقد تجاوزت TypeScript كلاً من Python وJavaScript في أغسطس 2025 لتصبح أكثر لغة مستخدمة على GitHub، ويذكر التقرير أن أغلب أطر الواجهات الكبيرة تنشئ المشاريع الجديدة بـ TypeScript افتراضياً. وفي 2026 صدر [الإصدار TypeScript 7](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/?ref=arabroot.io)، وهو إعادة كتابة للمترجم بلغة Go بدلاً من JavaScript، ويقول الفريق إنه أسرع من 8 إلى 12 مرة في البناء الكامل، وهذا هو الإصدار الذي استخدمناه في التجربة.

![جدول من ثلاثة صفوف وأربعة أعمدة: الأعمدة أثناء الكتابة في المحرر وعند تشغيل tsc في CI وفي الاختبارات وفي الإنتاج، والصفوف JavaScript فقط حيث تصل الأخطاء التسعة B1 إلى B9 إلى الإنتاج، ثم strict false حيث يكشف المترجم B2 وB6 فقط، ثم strict true مع noUncheckedIndexedAccess حيث يكشف سبعة أخطاء ويبقى B8 وB9 للاختبارات والتحقق وقت التشغيل](https://arabroot.io/content/images/2026/10/typescript-for-devs-01-where-bugs.webp)

نفس الأخطاء التسعة: أين تظهر بدون فحص، ومع الوضع المتساهل، ومع الوضع الصارم

## كيف يعمل المترجم tsc؟ الفحص Type Checking والإخراج Emit

عندما تشغل الأمر `tsc` فإنه يقوم بعملين منفصلين تماماً، ومن المهم أن تفصل بينهما في ذهنك. العمل الأول هو الفحص Type Checking، حيث يقرأ المترجم كل الملفات ويحللها إلى شجرة Syntax Tree، ثم يربط كل اسم بتعريفه Binding، ثم يحسب نوع كل تعبير في الكود ويقارنه بالنوع المتوقع في مكانه، وإذا وجد تعارضاً يطبع رسالة خطأ مع رقمها، مثل `TS2322`. والعمل الثاني هو الإخراج Emit، حيث يكتب ملفات JavaScript بعد حذف كل ما هو نوع، وقد يحول بعض الصيغ الحديثة إلى صيغ أقدم إذا طلبت منه ذلك في الخيار `target`.

والنقطة التي يتفاجأ بها كثير من المطورين أن العمل الثاني لا يعتمد على نتيجة الأول، فالمترجم يخرج ملفات JavaScript حتى لو وجد أخطاء في الأنواع، لأن الأنواع لا تغير معنى الكود، وإذا أردت أن يتوقف الإخراج عند أي خطأ فعليك بتفعيل الخيار `noEmitOnError`. وفي أغلب المشاريع الحديثة لا يقوم `tsc` بالإخراج أصلاً، فأداة البناء مثل Vite أو esbuild تحذف الأنواع بسرعة دون أن تفحصها، ويعمل `tsc --noEmit` في خط CI كخطوة فحص فقط. وهذا التقسيم منطقي لأن حذف الأنواع عملية بسيطة تتم ملفاً بملف، أما الفحص فيحتاج أن يرى المشروع كله ليعرف نوع كل دالة مستوردة.

### الأنواع البنيوية Structural Typing: الشكل وليس الاسم

في لغات مثل Java وC# يكون النوع مرتبطاً باسمه، فالكلاس `Product` لا يقبل مكانه كلاس آخر له نفس الحقول إلا إذا ورثه أو طبق نفس الواجهة صراحة، وهذا ما يسمى الأنواع الاسمية Nominal Typing. أما TypeScript فتقارن الشكل فقط، فإذا كانت الدالة تنتظر `CartItem`، وهو كائن فيه `productId` نصي و`quantity` رقمي، فإن أي كائن له هذان الحقلان بهذين النوعين يقبل، مهما كان اسمه ومن أين جاء. ويشرح دليل اللغة هذا في صفحة [توافق الأنواع Type Compatibility](https://www.typescriptlang.org/docs/handbook/type-compatibility.html?ref=arabroot.io).

وهذا الاختيار مناسب جداً لـ JavaScript، لأن الكود فيها مليء بالكائنات التي تنشأ مباشرة بالأقواس `{}` دون كلاس، وبالردود التي تأتي من JSON. ولكن له ثمن سوف تراه في الخطأ B8 في التجربة، فالدالة `applyCoupon(total: number, percent: number)` تقبل أي رقمين بأي ترتيب، لأن «المجموع» و«النسبة» في نظر المترجم شيء واحد اسمه `number`. فإذا أرسل أحدهم النسبة مكان المجموع فلن يعترض المترجم أبداً، وسوف نرى بعد قليل كيف تتجنب هذا.

### الاستنتاج Type Inference: لماذا لا تكتب النوع في كل سطر؟

لا تحتاج في TypeScript أن تكتب النوع بجانب كل متغير، فعندما تكتب `let total = 0` يستنتج المترجم أن النوع `number`، وعندما تكتب `products.find(...)` يعرف أن النتيجة `Product | undefined` لأن تعريف `find` في ملفات المكتبة القياسية يقول ذلك. ولهذا السبب ترى الكود المكتوب جيداً بـ TypeScript قريباً جداً من JavaScript، فالأنواع تكتب صراحة في الحدود فقط: معاملات الدوال، وأشكال البيانات المشتركة، وما ترجعه الدوال العامة. وفي التجربة لاحظ أن المترجم قرأ 67 ملفاً و56,283 سطراً لكي يفحص أربعة ملفات صغيرة، والسبب أن أغلب هذه الأسطر هي ملفات التعريف Declaration Files الخاصة بالمكتبة القياسية والـ DOM، وهي التي تعرف المترجم بأن `toFixed` موجودة على الأرقام وغير موجودة على النصوص.

ويوجد استثناء مهم للاستنتاج، وهو معاملات الدوال، فالمترجم لا يعرف نوع `value` في `function formatPrice(value)` لأنه لا يرى من سيستدعيها، فيعطيها النوع `any`، وهذا النوع يعني «لا تفحص شيئاً». وفي الوضع المتساهل يمر هذا بصمت، وفي الوضع الصارم يظهر الخطأ `TS7006` كما سوف تجد في مخرج التجربة، وهذا من أهم أسباب تفعيل الوضع الصارم.

## محو الأنواع Type Erasure: لا يبقى شيء وقت التشغيل

الأنواع في TypeScript تختفي تماماً بعد الإخراج، فلا يوجد في ملف JavaScript الناتج أي أثر للـ `interface` أو لنوع المعامل أو لنوع ما ترجعه الدالة. ويسمي الدليل هذا [الأنواع الممحوة Erased Types](https://www.typescriptlang.org/docs/handbook/2/basic-types.html?ref=arabroot.io#erased-types). وفي التجربة صار ملف `types.ts` الذي فيه كل الأشكال، وحجمه 417 بايت، ملفاً من 11 بايت فقط هو السطر `export {};`، لأن كل ما فيه أنواع. والصورة التالية تبين ما يحدث لدالة واحدة:

![ثلاثة صناديق: الكود المكتوب في cart.ts وفيه أنواع المعاملات ونوع الإرجاع وinterface مشطوبة بالأحمر، ثم tsc الذي يقوم بالفحص ثم الإخراج، ثم cart.js بدون أي أنواع، وتحتها أحجام الملفات قبل الإخراج وبعده: types من 417 إلى 11 بايت، وcatalog من 1141 إلى 1013، وcart من 1891 إلى 1689، وmain من 1241 إلى 1144، وملاحظة أن node --experimental-strip-types يحذف الأنواع ويشغل الملف دون فحص](https://arabroot.io/content/images/2026/10/typescript-for-devs-02-erasure.webp)

الفحص يحدث قبل التشغيل، وما يعمل فعلاً هو JavaScript بدون أنواع

ولهذا المحو نتيجتان عمليتان يجب أن تتذكرهما دائماً. الأولى أنك لا تستطيع أن تسأل عن النوع وقت التشغيل، فلا يوجد شيء اسمه `if (x instanceof CartItem)` عندما يكون `CartItem` واجهة `interface`، لأنها غير موجودة في الكود الذي يعمل، وكل فحص وقت التشغيل تكتبه بنفسك بـ `typeof` و`Array.isArray` وأمثالها. والثانية أن كل كود لا يمر على المترجم لا يستفيد من الأنواع إطلاقاً، سواء كان كود JavaScript قديماً يستدعي مكتبتك، أو رداً من سيرفر، أو قيمة قرأتها من `localStorage`. وهذا ما سوف نختبره في الخطوة الرابعة من التجربة.

ويوجد اليوم طريق آخر لتشغيل TypeScript بدون `tsc`، فمنذ Node.js 22.6 يوجد الخيار [\--experimental-strip-types](https://nodejs.org/docs/latest-v22.x/api/typescript.html?ref=arabroot.io) الذي يحذف الأنواع ثم يشغل الملف مباشرة، وفي إصدارات Node.js الأحدث صار مفعلاً افتراضياً. وهذا الخيار لا يفحص أي شيء، وإنما يستبدل الأنواع بمسافات فارغة ويشغل الباقي، ولذلك فإنه أوضح دليل على أن الأنواع لا تحمي الكود وقت التشغيل، وسوف نستخدمه في أول خطوة من التجربة لنرى ماذا يفعل الكود الذي فيه الأخطاء التسعة. ويوجد أيضاً [مقترح لإضافة صيغة الأنواع إلى JavaScript نفسها](https://github.com/tc39/proposal-type-annotations?ref=arabroot.io) كتعليقات يتجاهلها المحرك، وهو مبني على نفس الفكرة: الأنواع للأدوات، وليست للمحرك.

## الوضع الصارم Strict Mode: ماذا يفعل كل خيار؟

الخيار [strict](https://www.typescriptlang.org/tsconfig/?ref=arabroot.io#strict) في ملف `tsconfig.json` ليس خياراً واحداً، وإنما مفتاح يفعل مجموعة من الخيارات دفعة واحدة، وكل واحد منها يغلق باباً تمر منه أخطاء معينة. وأهمها ما يلي:

- **التحقق من القيم الفارغة strictNullChecks:** بدونه يعتبر المترجم أن `null` و`undefined` قيمة مقبولة لأي نوع، فالدالة التي ترجع `Product | undefined` تعامل كأنها ترجع `Product` دائماً، وتمر كل الأخطاء من نوع «Cannot read properties of undefined». وهذا الخيار وحده هو الذي كشف B1 وB4 وB5 في التجربة.
- **منع any الضمني noImplicitAny:** يرفض أي معامل أو متغير لا يستطيع المترجم استنتاج نوعه فيعطيه `any` بصمت، وهو الذي كشف B3.
- **فحص أنواع الدوال strictFunctionTypes:** يفحص معاملات الدوال التي تمرر كقيم بشكل أدق، فلا تمر دالة تنتظر `Product` مكان دالة ستستقبل أي كائن.
- **الأخطاء في catch كـ unknown useUnknownInCatchVariables:** يجعل المتغير في `catch (e)` من النوع `unknown`، لأن JavaScript تسمح برمي أي قيمة وليس `Error` فقط، فلا تستطيع أن تكتب `e.message` قبل أن تتحقق.
- **تهيئة خصائص الكلاس strictPropertyInitialization:** يرفض خاصية في الكلاس نوعها لا يقبل `undefined` ولا تعطيها قيمة في الـ constructor.

وفي [الإصدار TypeScript 6.0](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/?ref=arabroot.io) تغير شيء مهم، فقد صار `strict` مفعلاً افتراضياً، ويقول الفريق إن أغلب المشاريع الجديدة تريده مفعلاً، ومن كان يعتمد على القيمة القديمة `false` فعليه أن يكتبها صراحة. وهذا التغيير انتقل إلى TypeScript 7، وظهر لنا في التجربة كما سوف ترى، فالملف الذي لا يذكر `strict` أعطى نفس نتيجة الوضع الصارم تماماً.

### خيار إضافي خارج strict: noUncheckedIndexedAccess

بعض الخيارات المفيدة لا يفعلها `strict`، وأهمها [noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig/?ref=arabroot.io#noUncheckedIndexedAccess). فعندما تكتب `order.items[0]` يعتبر المترجم افتراضياً أن النتيجة من النوع `CartItem`، مع أن المصفوفة قد تكون فارغة، وفي هذه الحالة تكون النتيجة `undefined`. ومع هذا الخيار يصبح نوع أي وصول بالفهرس `CartItem | undefined`، فيجبرك المترجم أن تتعامل مع حالة الفراغ. والسبب في أنه ليس ضمن `strict` أنه مزعج في الكود الذي يمر على المصفوفة بحلقة `for` بالفهرس، حيث يعرف المطور أن الفهرس صحيح ولا يعرفه المترجم، ولكنه في كود الواجهات الذي يأخذ أول عنصر من نتيجة بحث أو آخر رسالة في محادثة يكشف أخطاء حقيقية، كما كشف B7 في التجربة.

## التضييق Narrowing والأنواع المتحدة Union Types

النوع المتحد Union Type مثل `Product | undefined` أو `"pending" | "paid" | "shipped" | "cancelled"` هو الطريقة التي تقول بها للمترجم إن القيمة قد تكون واحداً من عدة أشكال. ولكن هذا وحده يجعل الكود مزعجاً، فكيف تصل إلى `product.price` إذا كان `product` قد يكون `undefined`؟ والحل هو التضييق Narrowing، أي أن المترجم يتتبع الشروط في الكود سطراً بسطر، فبعد `if (!product) throw ...` يعرف أن الفرع الذي فيه `undefined` قد خرج من الدالة، فيضيق النوع في الأسطر التالية إلى `Product`. ويشرح [فصل التضييق في الدليل](https://www.typescriptlang.org/docs/handbook/2/narrowing.html?ref=arabroot.io) كل الطرق التي يفهمها المترجم: `typeof` و`instanceof` و`in` والمقارنة بقيمة محددة والدوال التي ترجع `x is Product`.

![عمودان: الأول يتتبع نوع product من Product أو undefined، ثم بعد if (!product) throw يخرج undefined، ثم يصبح Product عند حساب المجموع، والثاني يتتبع status في switch من أربع قيم، وبعد ثلاث حالات يبقى cancelled، وبعد الحالة الرابعة يصبح النوع never في default، وإذا أضيفت حالة جديدة يتوقف البناء](https://arabroot.io/content/images/2026/10/typescript-for-devs-04-narrowing.webp)

كل شرط يحذف احتمالاً، وما يبقى بعد آخر حالة هو never

والتطبيق الأجمل للتضييق هو فحص اكتمال الحالات Exhaustiveness Checking. فالدالة `statusLabel` في التجربة تحول حالة الطلب إلى نص عربي، والمطور الأول كتب ثلاث حالات ونسي الرابعة `"cancelled"`. وفي الوضع الصارم يرفض المترجم الدالة لأن نوعها المعلن `string` ولكن أحد الطرق فيها يصل إلى نهايتها دون `return`، أي يرجع `undefined`. والإصلاح الأفضل من مجرد إضافة الحالة هو أن تضيف فرع `default` يسند القيمة إلى متغير من النوع `never`، فإذا أضاف زميلك بعد شهر حالة جديدة مثل `"refunded"` إلى النوع `OrderStatus` ونسي هذه الدالة، فإن البناء يتوقف عند هذا السطر بالضبط، لأن القيمة لم تعد `never`. وبهذا الشكل تحول المترجم إلى قائمة مراجعة تمر على كل مكان في الكود يعتمد على الحالات.

## التجربة العملية Lab: تسعة أخطاء Bugs في سلة المشتريات

الآن سوف نختبر كل ما سبق. المشروع أربعة ملفات صغيرة تمثل جزءاً من واجهة المتجر: الأنواع، والكتالوج، ودوال السلة، وملف يشغل كل حالة ويطبع النتيجة. وزرعنا في دوال السلة تسعة أخطاء من النوع الذي يظهر في المشاريع الحقيقية، وكل خطأ معلم بتعليق من B1 إلى B9\. والتجربة تحتاج Node.js 22.6 أو أحدث، والحزمة `typescript`، ولا تحتاج أي شيء آخر.

### المشروع وملفات الإعداد tsconfig

نبدأ بالأنواع، وهي الأشكال التي تتفق عليها كل الملفات:

```typescript
// Shapes of the store's data. These lines vanish completely when the code runs.
export type OrderStatus = "pending" | "paid" | "shipped" | "cancelled";

export interface Product {
  id: string;
  name: string;
  price: number;   // riyals
  stock: number;
}

export interface CartItem {
  productId: string;
  quantity: number;
}

export interface Order {
  id: string;
  status: OrderStatus;
  items: CartItem[];
}
```

ثم الكتالوج، وفيه بحث عن منتج، وفحص للمخزون بدالة غير متزامنة لأنها في الواقع تسأل السيرفر، ودالة تقرأ الكتالوج من نص JSON:

```typescript
import type { Product } from "./types.ts";

const products: Product[] = [
  { id: "p1", name: "قهوة عربية", price: 45, stock: 10 },
  { id: "p2", name: "تمر سكري", price: 30, stock: 0 },
];

export function findProduct(id: string): Product | undefined {
  return products.find((p) => p.id === id);
}

export async function inStock(id: string): Promise<boolean> {
  return (findProduct(id)?.stock ?? 0) > 0;
}

// The server sends the catalogue as JSON text
export function parseProducts(json: string): Product[] {
  return JSON.parse(json);
}
```

ثم دوال السلة، وهي الملف الذي فيه الأخطاء:

```typescript
import type { CartItem, Order, OrderStatus, Product } from "./types.ts";
import { findProduct, inStock } from "./catalog.ts";

// B1: a product id that is not in the catalogue
export function cartTotal(items: CartItem[]): number {
  let total = 0;
  for (const item of items) {
    const product = findProduct(item.productId);
    total += product.price * item.quantity;
  }
  return total;
}

// B2: a typo in a property name
export function itemCount(items: CartItem[]): number {
  return items.reduce((n, item) => n + item.quantiy, 0);
}

// Correct code, used to show B6
export function totalQuantity(items: CartItem[]): number {
  return items.reduce((n, item) => n + item.quantity, 0);
}

// B3: a parameter without a type
export function formatPrice(value) {
  return value.toFixed(2) + " ر.س";
}

// B4: a status the switch forgot
export function statusLabel(status: OrderStatus): string {
  switch (status) {
    case "pending": return "بانتظار الدفع";
    case "paid": return "مدفوع";
    case "shipped": return "تم الشحن";
  }
}

// B5 and B6: a forgotten await, and the quantity as typed in the form
export async function addToCart(items: CartItem[], productId: string, qty: string) {
  if (inStock(productId)) {
    items.push({ productId, quantity: qty });
  }
}

// B7: the first item of an order that may be empty
export function firstProductId(order: Order): string {
  return order.items[0].productId;
}

// B8: two numbers in the wrong order
export function applyCoupon(total: number, percent: number): number {
  return total - (total * percent) / 100;
}

// B9: the sum of prices that came from JSON
export function catalogueValue(products: Product[]): number {
  return products.reduce((sum, p) => sum + p.price, 0);
}
```

وأخيراً الملف الذي يشغل الحالات، وكل سطر `run` فيه يمثل استخداماً حقيقياً لدالة من الدوال السابقة:

```typescript
import { parseProducts } from "./catalog.ts";
import { addToCart, applyCoupon, cartTotal, catalogueValue, firstProductId, formatPrice, itemCount, statusLabel, totalQuantity } from "./cart.ts";
import type { CartItem } from "./types.ts";

function run(name: string, fn: () => unknown) {
  try {
    const v = fn();
    console.log(name.padEnd(4), "=>", typeof v === "string" ? JSON.stringify(v) : String(v));
  } catch (e) {
    console.log(name.padEnd(4), "=> THROWS", (e as Error).constructor.name + ":", (e as Error).message);
  }
}

const cart: CartItem[] = [{ productId: "p1", quantity: 2 }];

run("B1", () => cartTotal([...cart, { productId: "p9", quantity: 1 }]));
run("B2", () => itemCount(cart));
run("B3", () => formatPrice("90"));
run("B4", () => statusLabel("cancelled"));
await addToCart(cart, "p2", "3");          // p2 is out of stock
run("B5", () => cart.length);
run("B6", () => totalQuantity(cart));
run("B7", () => firstProductId({ id: "o1", status: "pending", items: [] }));
run("B8", () => applyCoupon(10, 90));       // meant: 10% off 90 riyals
run("B9", () => catalogueValue(parseProducts('[{"id":"p1","name":"قهوة","price":"45","stock":3},{"id":"p3","name":"هيل","price":"20","stock":5}]')));
```

أما الإعدادات فهي ملف أساسي مشترك، ثم أربعة ملفات صغيرة يختلف كل منها في خيار واحد. والملف الأساسي كما يلي:

```json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "outDir": "dist",
    "noEmit": true
  },
  "include": ["src"]
}
```

وفي الإعداد أعلاه لاحظ التالي:

- `"module": "nodenext"` يجعل المترجم يتبع قواعد Node.js في الاستيراد، ومنها أن الملف المستورد يكتب بامتداده.
- الاستيراد في الملفات مكتوب بالامتداد `.ts`، وهذا ما يحتاجه Node.js عندما يشغل الملفات مباشرة، والخيار [rewriteRelativeImportExtensions](https://www.typescriptlang.org/tsconfig/?ref=arabroot.io#rewriteRelativeImportExtensions) يحول هذا الامتداد إلى `.js` عند الإخراج، حتى يعمل نفس الكود بالطريقتين.
- `"noEmit": true` لأننا في الملفات الأولى نريد الفحص فقط، وسوف نغيره في خطوة الإصلاح.

والملفات الأربعة هي `tsconfig.loose.json` وفيه `"strict": false`، و`tsconfig.default.json` الذي لا يذكر `strict` أصلاً، و`tsconfig.strict.json` وفيه `"strict": true`، و`tsconfig.max.json` وفيه `strict` مع `noUncheckedIndexedAccess`. وكل التجربة يشغلها السكربت `run.sh` الموجود مع المقال، والأوامر الأساسية فيه كما يلي:

```bash
npm i -D typescript@7.0.2
node --no-warnings --experimental-strip-types src/main.ts
npx tsc -p tsconfig.loose.json --pretty false
npx tsc -p tsconfig.default.json --pretty false
npx tsc -p tsconfig.strict.json --pretty false
npx tsc -p tsconfig.max.json --pretty false
```

### الخطوة الأولى: الكود يعمل كأنه JavaScript

قبل أن نفحص أي شيء، نشغل الكود كما هو بالأمر `node --experimental-strip-types`، أي أن Node.js يحذف الأنواع ويشغل الباقي، وهذا بالضبط ما يحدث في مشروع JavaScript لا يوجد فيه فحص. والمخرج سوف يكون كما يلي:

```text
## node: v22.14.0   tsc: Version 7.0.2

## 1) the buggy code runs as plain JavaScript (types stripped)
B1   => THROWS TypeError: Cannot read properties of undefined (reading 'price')
B2   => NaN
B3   => THROWS TypeError: value.toFixed is not a function
B4   => undefined
B5   => 2
B6   => "23"
B7   => THROWS TypeError: Cannot read properties of undefined (reading 'productId')
B8   => 1
B9   => "04520"
```

وفي المخرج أعلاه لاحظ التالي:

- **ثلاثة أخطاء فقط رمت استثناء**، وهي B1 وB3 وB7، وهذه في الحقيقة الأخطاء «المحظوظة»، لأنها تظهر في سجل الأخطاء ويراها أحد.
- **B2 أعطى `NaN`**، فالخاصية `quantiy` غير موجودة وقيمتها `undefined`، و`0 + undefined` تعطي `NaN`، وهذه القيمة سوف تظهر للعميل كعدد القطع في السلة دون أي خطأ.
- **B4 أرجع `undefined`**، أي أن الطلب الملغي سوف يظهر في الواجهة بدون حالة، أو بكلمة «undefined» إذا طبعته الواجهة مباشرة.
- **B5 أعطى 2**، مع أن التمر نفد من المخزون، والسبب أن `inStock(productId)` بدون `await` ترجع Promise، وأي Promise في شرط `if` تعتبر قيمة صحيحة Truthy، فالشرط صحيح دائماً. وإذا أردت أن تفهم لماذا لا ينتظر الكود هنا، فقد شرحنا الوعود Promises وترتيب تنفيذها في [مقال حلقة الأحداث Event Loop](https://arabroot.io/articles/%D8%AD%D9%84%D9%82%D8%A9-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-event-loop-%D9%81%D9%8A-javascript/).
- **B6 أعطى النص `"23"`**، لأن الكمية جاءت من النموذج نصاً `"3"`، و`2 + "3"` في JavaScript تعطي `"23"`.
- **B8 أعطى 1**، وهذا هو خطأ الكوبون من القصة، فالمقصود خصم 10% من 90 ريالاً، أي 81، ولكن الرقمين مرا بترتيب معكوس، فحسبت الدالة خصم 90% من 10 ريالات.
- **B9 أعطى النص `"04520"`**، فالسيرفر أرسل الأسعار نصوصاً `"45"` و`"20"`، فجمعت `reduce` الصفر مع النصين كنصوص.

أي أن ستة أخطاء من تسعة لا ترمي أي استثناء، وإنما تعطي قيمة خاطئة تمر إلى الخطوة التالية، وهذا النوع هو الأخطر في المتاجر والأنظمة المالية، لأن أحداً لا يلاحظه حتى يراجع حساباً أو يشتكي عميل.

### الخطوة الثانية: tsc بالوضع المتساهل ثم الصارم

الآن نفحص نفس الكود بالمترجم، ونبدأ بالوضع المتساهل `"strict": false`:

```text
## 2) tsc -p tsconfig.loose.json
src/cart.ts(16,45): error TS2551: Property 'quantiy' does not exist on type 'CartItem'. Did you mean 'quantity'?
src/cart.ts(41,29): error TS2322: Type 'string' is not assignable to type 'number'.
exit code: 1
```

خطآن فقط من تسعة: الخطأ الإملائي B2، والمترجم يقترح الاسم الصحيح لأنه يعرف حقول `CartItem`، والكمية النصية B6، لأن الحقل `quantity` معرف كرقم والقيمة `qty` معرفة كنص. وكل رسالة تبدأ بالملف ثم السطر والعمود بين قوسين، ثم رقم الخطأ، وهذا الرقم مفيد عندما تبحث عن شرح الخطأ. وقيمة `exit code: 1` مهمة في CI، لأنها تجعل خطوة الفحص تفشل فيتوقف الدمج.

ثم الوضع الصارم `"strict": true`:

```text
## 2) tsc -p tsconfig.strict.json
src/cart.ts(9,14): error TS18048: 'product' is possibly 'undefined'.
src/cart.ts(16,45): error TS2551: Property 'quantiy' does not exist on type 'CartItem'. Did you mean 'quantity'?
src/cart.ts(25,29): error TS7006: Parameter 'value' implicitly has an 'any' type.
src/cart.ts(30,51): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
src/cart.ts(40,7): error TS2801: This condition will always return true since this 'Promise<boolean>' is always defined.
src/cart.ts(41,29): error TS2322: Type 'string' is not assignable to type 'number'.
exit code: 1
```

صارت ستة أخطاء، والأربعة الجديدة كلها بسبب خيارين فقط من خيارات `strict`:

- `TS18048` في السطر 9: `findProduct` ترجع `Product | undefined`، والكود يقرأ `product.price` دون أن يتحقق، وهذا هو B1، وكشفه `strictNullChecks`.
- `TS7006` في السطر 25: المعامل `value` بدون نوع، فهو `any` ضمنياً، وهذا هو B3، وكشفه `noImplicitAny`. ولاحظ أن الخطأ ظهر على تعريف الدالة، وليس على السطر الذي يرسل لها النص `"90"`، لأن المترجم لا يعرف ما الذي يجب أن يعترض عليه في الاستدعاء ما دام المعامل `any`.
- `TS2366` في السطر 30: الدالة تعلن أنها ترجع `string`، ولكن يوجد طريق يصل إلى نهايتها، وهذا هو B4.
- `TS2801` في السطر 40: هذا من أجمل رسائل المترجم، فهو يقول إن الشرط صحيح دائماً لأن الـ Promise موجودة دائماً، وهذا هو B5، أي الـ `await` المنسية.

وأخيراً الإعداد الأقصى مع `noUncheckedIndexedAccess`:

```text
## 2) tsc -p tsconfig.max.json
src/cart.ts(9,14): error TS18048: 'product' is possibly 'undefined'.
src/cart.ts(16,45): error TS2551: Property 'quantiy' does not exist on type 'CartItem'. Did you mean 'quantity'?
src/cart.ts(25,29): error TS7006: Parameter 'value' implicitly has an 'any' type.
src/cart.ts(30,51): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
src/cart.ts(40,7): error TS2801: This condition will always return true since this 'Promise<boolean>' is always defined.
src/cart.ts(41,29): error TS2322: Type 'string' is not assignable to type 'number'.
src/cart.ts(47,10): error TS2532: Object is possibly 'undefined'.
exit code: 1
```

الخطأ السابع `TS2532` في السطر 47 هو B7، أي `order.items[0]` على طلب قد يكون فارغاً. وبهذا يكون المترجم قد أمسك سبعة أخطاء من تسعة قبل أن يعمل سطر واحد من الكود، أما B8 وB9 فلم يظهرا في أي إعداد، وسوف نخصص لهما قسماً كاملاً بعد التجربة. والصورة التالية تجمع النتيجة:

![جدول بالأخطاء التسعة: لكل خطأ وصف قصير، والنتيجة وقت التشغيل مثل TypeError وNaN وundefined و23 و04520، ثم ثلاثة أعمدة لما كشفه tsc في strict false وstrict true ومع noUncheckedIndexedAccess برموز الأخطاء TS18048 وTS2551 وTS7006 وTS2366 وTS2801 وTS2322 وTS2532، وB8 وB9 لم يكشفهما أي إعداد](https://arabroot.io/content/images/2026/10/typescript-for-devs-03-bug-matrix.webp)

رمز الخطأ الذي أعطاه المترجم لكل خطأ في كل إعداد

### الخطوة الثالثة: الإصلاح ثم الإخراج Emit

الآن نصلح الأخطاء في مجلد منفصل `fixed`، ونفعل الإخراج حتى نرى ملفات JavaScript الناتجة. والتغييرات المهمة في `cart.ts` كما يلي:

```typescript
export function cartTotal(items: CartItem[]): number {
  let total = 0;
  for (const item of items) {
    const product = findProduct(item.productId);
    if (!product) throw new Error(`unknown product ${item.productId}`);
    total += product.price * item.quantity;
  }
  return total;
}

export function statusLabel(status: OrderStatus): string {
  switch (status) {
    case "pending": return "بانتظار الدفع";
    case "paid": return "مدفوع";
    case "shipped": return "تم الشحن";
    case "cancelled": return "ملغي";
    default: {
      const unreachable: never = status;   // a new status added later fails here at compile time
      return unreachable;
    }
  }
}

export async function addToCart(items: CartItem[], productId: string, qty: string) {
  const quantity = Number.parseInt(qty, 10);
  if (!Number.isInteger(quantity) || quantity < 1) throw new Error(`bad quantity ${qty}`);
  if (await inStock(productId)) {
    items.push({ productId, quantity });
  }
}

export function firstProductId(order: Order): string | undefined {
  return order.items[0]?.productId;
}

// Named fields: the caller cannot swap two numbers by accident
export function applyCoupon({ total, percent }: { total: number; percent: number }): number {
  return total - (total * percent) / 100;
}
```

ثم نشغل المترجم بالملف `tsconfig.fixed.json`، وفيه `strict` و`noUncheckedIndexedAccess` و`"noEmit": false`، ثم نشغل ملفات JavaScript الناتجة مباشرة بـ `node`:

```bash
npx tsc -p tsconfig.fixed.json --pretty false
node dist/main.js
```

والمخرج سوف يكون كما يلي:

```text
## 3) fixed version: compile, then run the emitted JavaScript
exit code: 0
B1   => THROWS Error: unknown product p9
B2   => 2
B3   => "90.00 ر.س"
B4   => "ملغي"
B5   => 1
B6   => 2
B7   => undefined
B8   => 81
B9   => THROWS Error: catalogue item 0 is not a Product: {"id":"p1","name":"قهوة","price":"45","stock":3}
```

وفي المخرج أعلاه لاحظ التالي:

- B1 ما زال يرمي استثناء، ولكنه الآن استثناء نحن كتبناه برسالة واضحة `unknown product p9`، بدلاً من `Cannot read properties of undefined` الذي لا يقول لك أي منتج هو. والمترجم لم يجبرنا على رمي استثناء تحديداً، وإنما أجبرنا أن نقرر ماذا نفعل عندما لا يوجد المنتج.
- B5 صار 1، أي أن التمر الذي نفد لم يضف إلى السلة، وB6 صار رقماً لأن الكمية تحولت إلى رقم وتم التحقق منها قبل أن تدخل السلة.
- B7 صار `undefined` بدلاً من استثناء، لأن نوع ما ترجعه الدالة صار `string | undefined`، وكل من يستدعيها سوف يجبره المترجم أن يتعامل مع الطلب الفارغ.
- B8 صار 81، والإصلاح هنا ليس من الأنواع، وإنما من شكل الدالة نفسها، فصارت تأخذ كائناً بحقلين مسميين `{ total, percent }`، ومن المستحيل أن تكتب `percent: 90` وأنت تقصد المجموع دون أن تلاحظ.
- B9 صار يرمي استثناء عند أول عنصر في الكتالوج، وهذا جاء من دالة تحقق كتبناها بأنفسنا، وسوف نشرحها في القسم القادم.

أما حجم الملفات قبل الإخراج وبعده فكان كما يلي، والأرقام بالبايت:

```text
## 4) bytes: TypeScript source vs emitted JavaScript
types      417 ->    11
catalog   1141 ->  1013
cart      1891 ->  1689
main      1241 ->  1144
```

والملف `types.ts` اختفى تقريباً، لأن كل ما فيه أنواع، والملفات الأخرى صغرت بمقدار الأنواع المكتوبة فيها فقط، وهذا يعني أن TypeScript لا تضيف أي كود إلى ما يصل إلى المتصفح، فلا تدفع شيئاً في حجم الصفحة مقابل الأنواع. وإذا فتحت `dist/cart.js` فسوف تجد نفس الكود الذي كتبته بدون الأنواع، حتى التعليقات بقيت في مكانها.

### الخطوة الرابعة: كود JavaScript قديم يستدعي الكود المترجم

الآن نتأكد من فكرة المحو بأنفسنا، فنكتب ملفاً بـ JavaScript عادية يمثل جزءاً قديماً من المشروع لم ينقل بعد، ويستدعي الدوال المترجمة بقيم خاطئة:

```javascript
// Old JavaScript code calling the compiled module: nothing checks the types here
import { formatPrice, applyCoupon } from "./dist/cart.js";
try { console.log(formatPrice("90")); } catch (e) { console.log("THROWS", e.message); }
console.log(applyCoupon({ total: "90", percent: 10 }));
```

والمخرج سوف يكون كما يلي:

```text
## 5) old JavaScript calling the compiled module
THROWS value.toFixed is not a function
81
```

الدالة `formatPrice` معرفة في TypeScript بأنها تأخذ `number`، ولكن هذا التعريف لم يعد موجوداً في `dist/cart.js`، فاستقبلت النص ورمت نفس الخطأ القديم. أما `applyCoupon` فأعطت 81 مع أن المجموع نص، والسبب أن الطرح والضرب في JavaScript يحولان النص إلى رقم تلقائياً، أي أن النتيجة صحيحة هنا بالصدفة فقط، ولو كان في الدالة جمع لظهر نص غريب كما في B9\. والدرس أن أي مكتبة تنشرها لغيرك، وأي دالة يستدعيها كود لا يمر على المترجم، تحتاج تحققاً وقت التشغيل في مدخلاتها العامة إذا كان الخطأ فيها مكلفاً.

### مشكلتان واجهناهما أثناء التجربة

المشكلة الأولى ظهرت في أول تشغيل، فقد كان الملف `tsconfig.loose.json` في البداية لا يذكر `strict` أصلاً، على أساس أن القيمة الافتراضية هي `false` كما كانت في كل الإصدارات القديمة، فأعطى ستة أخطاء بدلاً من اثنين، وهي نفس نتيجة الوضع الصارم. والسبب كما ذكرنا أن TypeScript 6.0 جعلت `strict` مفعلاً افتراضياً، وانتقل ذلك إلى 7.0\. لذلك صار في التجربة ملفان: `tsconfig.default.json` الذي لا يذكر الخيار، و`tsconfig.loose.json` الذي يكتب `false` صراحة. وإذا كنت ترقي مشروعاً قديماً من TypeScript 5 فانتبه لهذا، لأن أول بناء بعد الترقية قد يعطيك مئات الأخطاء الجديدة، وهي ليست أخطاء في الترقية، وإنما هي الأخطاء التي كان الوضع المتساهل يخفيها. والحل المؤقت أن تكتب `"strict": false` صراحة، ثم تفعل الخيارات واحداً بعد الآخر كما سوف نشرح في قسم النقل.

والمشكلة الثانية كانت في كود التجربة نفسه، فالدالة `run` كانت في البداية تطبع النتيجة بـ `JSON.stringify`، فظهرت نتيجة B2 هكذا: `null`. والسبب أن `JSON.stringify(NaN)` يعطي `null`، لأن صيغة JSON لا تعرف `NaN` أصلاً، أي أن أداة الطباعة نفسها أخفت الخطأ الذي نبحث عنه. فغيرنا الدالة لتطبع النصوص بـ `JSON.stringify` حتى تظهر علامات التنصيص حولها، والقيم الأخرى بـ `String`. ولاحظ أن هذه المشكلة نفسها تحدث في الإنتاج، فالـ API الذي يرسل `NaN` في JSON يرسل `null`، والواجهة التي تستقبله لا تعرف أن الأصل كان حساباً خاطئاً.

### ماذا يحدث إذا استخدمت enum مع Node.js؟

قلنا إن الأنواع تحذف ولا يبقى منها شيء، ولكن يوجد في TypeScript بعض الصيغ التي ليست أنواعاً فقط، وإنما تولد كوداً وقت التشغيل، وأشهرها `enum`. لنأخذ هذا الملف الصغير:

```typescript
export enum Status { Pending = "pending", Paid = "paid" }
export const label = (s: Status) => (s === Status.Paid ? "مدفوع" : "بانتظار الدفع");
console.log(label(Status.Paid));
```

عندما نشغله بـ `node --experimental-strip-types`، وعندما نفحصه بـ `tsc` مع الخيار `--erasableSyntaxOnly`، يكون المخرج كما يلي:

```text
## 6) what if: an enum
TypeScript enum is not supported in strip-only mode
whatif/status.ts(1,13): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
exit code: 1
```

رفض Node.js تشغيل الملف، والسبب أن وضع الحذف فقط Strip-Only لا يستطيع أن «يحذف» الـ `enum`، لأنها تحتاج كوداً جديداً يمثلها. وإذا أخرجنا نفس الملف بـ `tsc` فسوف نجد هذا الكود في الناتج:

```javascript
export var Status;
(function (Status) {
    Status["Pending"] = "pending";
    Status["Paid"] = "paid";
})(Status || (Status = {}));
```

أي أن `enum` تتحول إلى كائن حقيقي يعمل وقت التشغيل، وهذا يخالف القاعدة التي بنينا عليها المقال. ولهذا أضافت [TypeScript 5.8](https://devblogs.microsoft.com/typescript/announcing-typescript-5-8/?ref=arabroot.io) الخيار [erasableSyntaxOnly](https://www.typescriptlang.org/tsconfig/?ref=arabroot.io#erasableSyntaxOnly)، الذي يرفض كل صيغة لا يمكن حذفها ببساطة، مثل `enum` و`namespace` التي فيها كود، وخصائص الـ constructor المختصرة مثل `constructor(private x: number)`. والبديل العملي للـ `enum` هو ما استخدمناه في التجربة أصلاً: نوع متحد من نصوص `"pending" | "paid"`، فهو يعطيك نفس الفحص ونفس الإكمال التلقائي في المحرر، ويختفي كلياً بعد الإخراج. وإذا كان فريقك يشغل TypeScript مباشرة في Node.js، أو يستخدم أداة بناء تحذف الأنواع ملفاً بملف، ففعل هذا الخيار من أول يوم.

## ما لا تكشفه الأنواع: الحدود والتحقق وقت التشغيل Runtime Validation

الخطآن B8 وB9 مرا من كل الإعدادات، ولكل واحد منهما سبب مختلف، وفهم السببين هو أهم ما في هذا المقال، لأنه يحدد أين تحتاج أدوات أخرى غير الأنواع.

الخطأ B8 مر لأن الكود صحيح من ناحية الأنواع، فالدالة تنتظر رقمين وأخذت رقمين. والمترجم لا يعرف أن الرقم الأول «مجموع» والثاني «نسبة»، لأن الأنواع البنيوية لا تفرق بين رقمين. وهناك ثلاث طرق لتقليل هذا النوع من الأخطاء. الأولى ما فعلناه في الإصلاح: معاملات مسماة في كائن، وهي أبسط الطرق وأوضحها لمن يقرأ الكود. والثانية الأنواع المميزة Branded Types، حيث تعرف نوعاً مثل `type Riyals = number & { __brand: "Riyals" }` فلا يقبل المترجم رقماً عادياً مكانه إلا بعد أن يمر على دالة تنشئه، وهي مفيدة في المبالغ والمعرفات IDs التي تتشابه أنواعها. والثالثة، وهي التي لا غنى عنها، الاختبار Test الذي يقول «خصم 10% من 90 يعطي 81»، فهذا الاختبار يكشف B8 وكل أخطاء المنطق الأخرى التي لا تعرف عنها الأنواع شيئاً، مثل أن يكتب أحدهم النسبة `0.15` وهو يقصد 15%. وقد شرحنا أنواع الاختبارات ومتى تكتب كلاً منها في [مقال أنواع الاختبارات](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/).

أما B9 فمر لسبب أخطر، وهو أن الدالة `parseProducts` تقول للمترجم إنها ترجع `Product[]`، والمترجم صدقها، لأن `JSON.parse` ترجع النوع `any`، و`any` يقبل أن يسند إلى أي نوع دون فحص. أي أن الكود قال «أعدك أن هذه منتجات»، والسيرفر أرسل أسعاراً نصية، ولا أحد تحقق. وهذا هو الحد Boundary بين كودك والعالم الخارجي، وكل قيمة تعبر هذا الحد لا يعرف عنها المترجم إلا ما تقوله له أنت. والصورة التالية تبين أهم هذه الحدود في تطبيق الواجهة:

![أربعة مصادر للبيانات على اليمين: رد API بصيغة JSON، ونموذج HTML كل قيمه نصوص، وlocalStorage ورابط الصفحة، ومكتبة JavaScript بدون أنواع، تمر كلها عبر بوابة تحقق وقت التشغيل نوعها unknown مع دالة isProduct، ثم تدخل إلى الكود المكتوب بـ TypeScript، وتحتها نتيجتان: بدون تحقق ظهر مجموع الكتالوج 04520، ومع التحقق توقف الكود عند أول عنصر](https://arabroot.io/content/images/2026/10/typescript-for-devs-05-boundary.webp)

داخل الحدود تعمل الأنواع، وعند الحدود تحتاج تحققاً وقت التشغيل

والإصلاح الذي في المجلد `fixed` يتكون من أمرين. الأول أن نتيجة `JSON.parse` تسند إلى متغير من النوع `unknown` وليس `any`، والفرق بينهما كبير، فـ `any` يعني «لا تفحص شيئاً»، أما `unknown` فيعني «لا أعرف ما هذا، فلا تسمح لي باستخدامه حتى أتحقق». والثاني دالة تحقق من نوع خاص يسمى حارس النوع Type Guard، ترجع `x is Product`، فإذا رجعت `true` يضيق المترجم النوع إلى `Product`:

```typescript
// A type guard: checks at run time what the type only promises at compile time
function isProduct(x: unknown): x is Product {
  if (typeof x !== "object" || x === null) return false;
  const o = x as Record<string, unknown>;
  return typeof o.id === "string" && typeof o.name === "string"
    && typeof o.price === "number" && Number.isFinite(o.price)
    && Number.isInteger(o.stock);
}

export function parseProducts(json: string): Product[] {
  const data: unknown = JSON.parse(json);
  if (!Array.isArray(data)) throw new Error("catalogue is not an array");
  data.forEach((x, i) => {
    if (!isProduct(x)) throw new Error(`catalogue item ${i} is not a Product: ${JSON.stringify(x)}`);
  });
  return data;
}
```

ولاحظ أن حارس النوع نفسه وعد يقطعه المطور، فإذا كتبت `x is Product` ونسيت فحص حقل، فسوف يصدقك المترجم. لذلك فإن كتابة هذه الدوال يدوياً لكل شكل في المشروع متعبة وعرضة للنسيان، وفي المشاريع الحقيقية تستخدم الفرق مكتبة تحقق مثل [Zod](https://zod.dev/?ref=arabroot.io)، حيث تكتب المخطط Schema مرة واحدة، فتحصل منه على دالة التحقق وعلى النوع معاً، فلا يمكن أن يختلفا. وأياً كانت الطريقة، فالقاعدة واحدة: تحقق من البيانات مرة واحدة عند الحد، ثم ثق بالأنواع داخل الكود.

وأين تقع هذه الحدود في تطبيق واجهة عادي؟ أولها ردود الـ API، وهي الأهم، لأن السيرفر قد يتغير دون أن تتغير الواجهة، وقد شرحنا شكل الطلب والرد في [مقال HTTP](https://arabroot.io/articles/http-%D9%88-https-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/). وثانيها حقول النماذج، فكل قيمة في `input.value` نص، حتى لو كان الحقل `type="number"`. وثالثها `localStorage` ورابط الصفحة، لأن المستخدم يستطيع تعديلهما بيده، وقد تكون القيمة المحفوظة من إصدار قديم من تطبيقك. ورابعها مكتبات JavaScript التي لا تأتي بملفات تعريف، أو تأتي بملفات تعريف خاطئة، وهذا يحدث أكثر مما تتوقع.

⚠️

الكلمة `as` في TypeScript ليست تحويلاً، وإنما أمر للمترجم بأن يتوقف عن الفحص. فعندما تكتب `await res.json() as Product[]` فأنت لم تتحقق من شيء، وإنما قلت للمترجم «صدقني»، وهذا بالضبط ما حدث في B9\. ابحث في مشروعك عن `as` و`any` و`!` بعد المتغيرات، فكل واحدة منها مكان قد يمر منه خطأ دون أن يراه المترجم.

## كيف تنقل مشروع JavaScript قائماً إلى TypeScript؟

بالعودة إلى المتجر في بداية المقال، فالفريق لا يستطيع أن يتوقف شهراً ليعيد كتابة كل شيء، والخبر الجيد أن TypeScript مصممة للنقل التدريجي، ويشرح دليلها هذا في صفحة [النقل من JavaScript](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html?ref=arabroot.io). والترتيب الذي ينصح به كما يلي:

1. **أضف الفحص إلى JavaScript نفسها أولاً.** فعل `allowJs` و`checkJs` في `tsconfig.json`، أو ضع التعليق `// @ts-check` في أعلى ملف واحد، وسوف يفحص المترجم ملفات `.js` مستنتجاً الأنواع من الكود ومن تعليقات JSDoc مثل `/** @param {number} total */`، وهذا يعطيك جزءاً من الفائدة دون أن تغير امتداد أي ملف. ويشرح الدليل ذلك في صفحة [TypeScript لملفات JavaScript](https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html?ref=arabroot.io).
2. **ابدأ بالأشكال المشتركة.** اكتب ملف الأنواع الذي يصف بيانات النظام: المنتج والطلب والمستخدم وردود الـ API، فهذا الملف وحده يغطي أغلب الأخطاء من نوع B2، لأن كل ملف يستورده يصبح محكوماً بنفس الأسماء.
3. **انقل الملفات من الأطراف إلى المركز.** ابدأ بالدوال المساعدة التي لا تعتمد على غيرها، ثم الوحدات التي تستخدمها، فكل ملف تنقله يجعل ما يعتمد عليه أدق.
4. **فعل الخيارات الصارمة واحداً بعد الآخر.** ابدأ بـ `noImplicitAny`، ثم `strictNullChecks` وهو الأكثر فائدة والأكثر أخطاء، ثم `strict` كاملاً. وإذا كان عدد الأخطاء كبيراً فبعض الفرق تفعل الخيار للملفات الجديدة فقط، ثم تنقل الملفات القديمة على مراحل.
5. **أضف `tsc --noEmit` إلى CI من اليوم الأول**، حتى لو كان مع خيارات متساهلة، فالفحص الذي لا يوقف الدمج لا يلتزم به أحد بعد أسبوعين.

ولا تنس ملفات التعريف Declaration Files الخاصة بالمكتبات، فأغلب المكتبات اليوم تأتي بأنواعها داخلها، والباقي له حزمة منفصلة باسم `@types/اسم-المكتبة`. ويشرح الدليل كيف تكتب ملف `.d.ts` لمكتبة ليس لها أنواع في صفحة [ملفات التعريف](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html?ref=arabroot.io)، وتذكر أن هذا الملف وعد آخر لا يتحقق منه أحد، فإذا كتبته خطأ فسوف يصدقه المترجم.

## كم تكلف TypeScript؟ نقاط القوة ونقاط الضعف

لكل أداة ثمن، ومن الأمانة أن نذكر ثمن TypeScript بجانب فائدتها، حتى يكون قرار فريقك مبنياً على الصورة كاملة.

**نقاط القوة:** أولها ما رأيته في التجربة، فسبعة أخطاء من تسعة ظهرت قبل التشغيل مع رقم السطر وشرح المشكلة، وبعضها مثل الـ `await` المنسية صعب جداً أن تراه بالعين. وثانيها أن الأنواع توثيق لا يتقادم، فتعريف `CartItem` يقول للمطور الجديد شكل البيانات، والمترجم يضمن أنه ما زال صحيحاً، وهذا لا يحدث مع التعليقات وملفات التوثيق. وثالثها إعادة التسمية Refactoring الآمنة، فعندما تغير اسم حقل في النوع يظهر لك المترجم كل مكان يجب تعديله، وفي مشروع من مئات الملفات هذا وحده يكفي لكثير من الفرق. ورابعها أدوات المحرر، فالإكمال التلقائي والانتقال إلى التعريف والبحث عن الاستخدامات كلها تعتمد على نفس محلل الأنواع.

**نقاط الضعف:** أولها خطوة بناء إضافية، فالمتصفح لا يفهم TypeScript، ولا بد من أداة تحذف الأنواع قبل التشغيل، مع ملفات إعداد يجب أن يفهمها أحد في الفريق. وثانيها وقت الفحص في المشاريع الكبيرة، وهذا ما جاء TypeScript 7 لحله، حيث يذكر الفريق في إعلان الإصدار أن فتح ملف فيه خطأ في مشروع VS Code كان يستغرق قرابة 17.5 ثانية حتى يظهر الخطأ، وصار أقل من 1.3 ثانية. وثالثها منحنى التعلم، فالأنواع البسيطة سهلة، ولكن أنواع المكتبات المتقدمة مثل الأنواع المشروطة Conditional Types والأنواع المعممة Generics المتداخلة قد تصبح لغة برمجة كاملة داخل اللغة، ويضيع فيها المطور وقتاً في إقناع المترجم بدلاً من حل المشكلة. ويشرح الدليل هذه الأدوات في [فصل الأنواع من الأنواع](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html?ref=arabroot.io)، والأنسب أن تستخدمها في المكتبات المشتركة فقط، وأن يبقى كود التطبيق بسيطاً. ورابعها الثقة الزائدة، وهي أخطر نقطة، فالفريق الذي يرى المترجم ناجحاً قد يقلل الاختبارات أو يترك التحقق عند الحدود، ثم يتفاجأ بخطأ مثل B9 في الإنتاج.

ومتى لا تحتاج TypeScript؟ سكربت صغير من مئة سطر يكتبه شخص واحد ويشغله مرة، أو نموذج أولي Prototype سوف يرمى بعد أسبوع، وفي هذه الحالات قد يكفيك `// @ts-check` مع تعليقات JSDoc إذا أردت بعض الفحص دون خطوة بناء. أما مشروع واجهة يعمل عليه أكثر من مطور، ويتوقع أن يعيش أكثر من بضعة أشهر، فالحساب في صالح TypeScript بكل تأكيد.

## أخطاء شائعة

- **استخدام `any` لإسكات المترجم.** كل `any` يطفئ الفحص في كل مكان تصل إليه القيمة، وليس في السطر الذي كتبتها فيه فقط. وإذا لم تعرف النوع فاستخدم `unknown`، وتحقق قبل الاستخدام.
- **الثقة بـ `as` على ردود الـ API.** `res.json() as Order` لا يتحقق من شيء، وهذا هو B9 بالضبط. تحقق عند الحد بدالة أو مكتبة تحقق.
- **علامة التعجب `!` بعد كل قيمة قد تكون فارغة.** `findProduct(id)!.price` يقول للمترجم «أنا متأكد أنها موجودة»، فيعود B1 كما كان. استخدم الشرط أو `?.` مع قيمة بديلة.
- **إطفاء `strict` بعد الترقية بسبب كثرة الأخطاء ثم نسيانه.** هذه الأخطاء موجودة في الكود أصلاً، والإطفاء يخفيها فقط. أطفئه مؤقتاً مع خطة لتفعيل الخيارات واحداً بعد الآخر.
- **الاعتماد على `tsc` في المحرر فقط.** المحرر يفحص الملفات المفتوحة، أما الخطأ الذي يظهر في ملف آخر بسبب تغييرك فلن تراه إلا إذا شغلت `tsc --noEmit` على المشروع كله، والمكان الصحيح لذلك هو CI.
- **أداة البناء تحذف الأنواع ولا تفحصها.** Vite وesbuild وNode.js بوضع الحذف تشغل الكود حتى لو كان فيه أخطاء أنواع، فإذا لم يكن في خط البناء خطوة `tsc` منفصلة فأنت تكتب الأنواع للمحرر فقط.
- **رقمان أو نصان متجاوران بنفس النوع.** مثل `transfer(from: string, to: string)`، فالمترجم لا يفرق بينهما. استخدم كائناً بأسماء، أو أنواعاً مميزة للمعرفات.
- **تنسيق المبالغ يدوياً.** `toFixed(2) + " ر.س"` في التجربة يعمل، ولكنه لا يتبع قواعد تنسيق الأرقام في اللغة العربية، والأفضل `Intl.NumberFormat` كما شرحنا في [مقال التعريب i18n](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B9%D8%B1%D9%8A%D8%A8-i18n-%D9%88%D8%A7%D9%84%D9%86%D8%B5-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D8%A7%D9%84%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-bidi-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86/)، والأنواع لن تنبهك لهذا أبداً.

## قائمة تحقق Checklist: هل يستفيد مشروعك من الأنواع Types فعلاً؟

الجدول التالي يجمع ما رأيناه، ومن يمسك كل نوع من الأخطاء:

| نوع الخطأ                       | من يمسكه               | الإعداد أو الأداة                      |
| ------------------------------- | ---------------------- | -------------------------------------- |
| اسم خاصية خطأ، أو نوع خاطئ صريح | المترجم في أي وضع      | tsc --noEmit                           |
| قيمة قد تكون undefined أو null  | المترجم                | strictNullChecks                       |
| معامل بدون نوع                  | المترجم                | noImplicitAny                          |
| await منسية في شرط              | المترجم                | strictNullChecks                       |
| حالة منسية في switch            | المترجم                | نوع الإرجاع، أو never في default       |
| عنصر غير موجود في مصفوفة        | المترجم                | noUncheckedIndexedAccess               |
| قيمتان بنفس النوع بترتيب معكوس  | شكل الدالة والاختبارات | معاملات مسماة، أنواع مميزة، Unit Tests |
| بيانات من الخارج بشكل مختلف     | التحقق وقت التشغيل     | unknown مع Type Guard أو Zod           |
| خطأ في المنطق أو في الحساب      | الاختبارات والمراجعة   | Unit وIntegration Tests                |

وقبل أن تقول إن مشروعك «مكتوب بـ TypeScript» راجع النقاط التالية:

- هل `strict` مفعل، أو توجد خطة مكتوبة لتفعيله؟
- هل خطوة `tsc --noEmit` تعمل في CI وتوقف الدمج عند الفشل؟
- هل كل رد من الـ API وكل قيمة من `localStorage` تمر على تحقق قبل أن تأخذ نوعها؟
- كم مرة تظهر `any` و`as` و`!` في الكود، وهل كل واحدة منها لها سبب مكتوب؟
- هل `erasableSyntaxOnly` مفعل إذا كان الكود يعمل بأداة تحذف الأنواع ملفاً بملف؟
- هل يوجد اختبار لكل حساب مالي، بغض النظر عن الأنواع؟

## تدريب عملي

- أضف الحالة `"refunded"` إلى النوع `OrderStatus` في المجلد `fixed`، وشغل `tsc`، واقرأ رسالة الخطأ عند السطر الذي فيه `never`.
- اكتب نوعاً مميزاً `Riyals` ونوعاً `Percent`، وغير `applyCoupon` لتأخذهما، ثم حاول أن تمرر الرقمين بترتيب معكوس، ولاحظ أين يعترض المترجم.
- استبدل دالة `isProduct` بمخطط Zod، وقارن عدد الأسطر، وما يحدث إذا أضفت حقلاً جديداً إلى `Product` ونسيت تحديث التحقق في كل طريقة.
- غير `JSON.parse(json)` في النسخة الخاطئة إلى `JSON.parse(json) as unknown` ثم أسنده إلى `Product[]`، وشغل `tsc`، ولاحظ أن الخطأ B9 صار مكشوفاً.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- TypeScript تفحص افتراضاتك عن شكل البيانات قبل التشغيل، ثم تحذف كل الأنواع، فما يعمل في المتصفح أو Node.js هو JavaScript عادية ليس فيها أي فحص.
- في التجربة رمت ثلاثة أخطاء فقط من تسعة استثناء عند التشغيل، والستة الباقية أعطت قيمة خاطئة بصمت، مثل كوبون جعل فاتورة 90 ريالاً بريال واحد.
- الوضع المتساهل كشف خطأين، والوضع الصارم ستة، ومع `noUncheckedIndexedAccess` سبعة، و`strict` صار مفعلاً افتراضياً منذ TypeScript 6.0.
- الخطآن اللذان فاتا كل الإعدادات هما رقمان بنفس النوع بترتيب معكوس، وبيانات JSON بشكل غير متوقع، والأول علاجه شكل الدالة والاختبارات، والثاني التحقق وقت التشغيل عند الحدود.
- `any` و`as` و`!` أماكن يتوقف فيها الفحص، و`unknown` مع حارس نوع هو الطريقة الصحيحة لاستقبال ما لا تعرفه.
- الصيغ التي تولد كوداً مثل `enum` لا تعمل مع أدوات الحذف فقط، والخيار `erasableSyntaxOnly` يمنعها من البداية.

وفي مرحلة [هندسة الواجهات وتطبيقات الويب](https://arabroot.io/start/#stage-web-frontend) تجد المقالات التي تكمل هذا المقال، من حلقة الأحداث التي تفسر الـ `await` المنسية إلى أطر الواجهات التي تبنى اليوم بـ TypeScript:

[![](https://arabroot.io/content/images/2026/10/js-event-loop-cover-cover.webp)دليلحلقة الأحداث Event Loop: لماذا تتأخر النقرة في صفحتك؟كيف ترتب JavaScript المهام والوعود والرسم على خيط واحد، ولماذا تنتظر النقرة خلف مهمة طويلة، وكيف تقسم العمل لتبقى الصفحة سريعة.](https://arabroot.io/articles/%D8%AD%D9%84%D9%82%D8%A9-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-event-loop-%D9%81%D9%8A-javascript/)[![](https://arabroot.io/content/images/2026/10/browser-internals-dom-cover-cover.webp)دليلكيف يرسم المتصفح صفحتك؟ من HTML إلى البكسلاتماذا يفعل المتصفح بين وصول HTML وظهور الصفحة، ولماذا تتجمد صفحة فيها ألفا صف، وكيف تقيس كل مرحلة في Chrome وتصلحها.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84-dom-%D9%88%D8%A7%D9%84%D8%AA%D8%AE%D8%B7%D9%8A%D8%B7-%D9%88%D8%A7%D9%84%D8%B1%D8%B3%D9%85/)[![](https://arabroot.io/content/images/2026/10/frontend-frameworks-arch-cover-cover.webp)دليلReact و Vue و Svelte: إدارة الحالة State وكلفة كل نقرةبنينا نفس التطبيق العربي بألف منتج في React وVue وSvelte وقسنا حجم الحزمة وعدد مرات إعادة الرسم مع كل نقرة، لتعرف أين تضع الحالة وكيف تختار الإطار.](https://arabroot.io/articles/react-%D9%88-vue-%D9%88-svelte-%D9%88%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%AD%D8%A7%D9%84%D8%A9-state-management/)[![](https://arabroot.io/content/images/2026/10/i18n-l10n-arabic-cover-cover.webp)دليلالتعريب i18n للمطورين: لماذا ينقلب النص العربي في الواجهة؟لماذا ينقلب النص المختلط في الواجهة العربية وكيف تعزله، وكيف تنسق الأرقام والتواريخ وصيغ الجمع العربية بأدوات المتصفح نفسها.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B9%D8%B1%D9%8A%D8%A8-i18n-%D9%88%D8%A7%D9%84%D9%86%D8%B5-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D8%A7%D9%84%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-bidi-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/testing-types-cover-cover.webp)دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/)[![](https://arabroot.io/content/images/2026/10/api-design-cover-cover.webp)دليلكيف تصمم API يبقى متوافقاً مع عملائه بعد سنةمبادئ تصميم API يتطور دون أن يعطل التطبيقات التي تعتمد عليه، وكيف تختار الشكل المناسب لنظامك.](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة بالمترجم TypeScript 7.0.2 وNode.js v22.14.0 على Windows 11.