الوحدة 5 · إيصال TypeScript إلى الإنتاج
صرامة tsconfig التي تؤتي ثمارها
19 دقراءة 4 داختر بحكمة
الدرس في لمحة
اختر خيارات الـ compiler التي تجعل أنواعك جديرة بالثقة، من strict وnoUncheckedIndexedAccess إلى verbatimModuleSyntax وmoduleResolution bundler، وفعّلها في قاعدة كود قائمة دون إيقاف العمل.
- 1strict: خط الأساس
- 2خياران يتركهما strict
- 3خيارات الوحدات: قل ما تعنيه
- 4تفعيل الخيارات في قاعدة كود قائمة
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
كل نوع في هذه الدورة جدير بالثقة بقدر إعدادات الـ compiler التي خلفه فقط. نوع Order نفسه الذي يلتقط رقم تتبّع ناقصًا تحت tsconfig ما، يترك انهيارًا يمرّ تحت آخر. حين انضممت إلى عملية الترحيل التي قُدتها لاحقًا، كان في tsconfig القيمة strict: false، وكان الفريق فخورًا بتغطية TypeScript بنسبة 100%. معظم تلك التغطية كانت تفحص القليل جدًا.
هذا الدرس عن اختيار الخيارات عن قصد، وعن تفعيلها في قاعدة كود حقيقية دون تجميد للعمل ستة أسابيع.
strict: خط الأساس
strict ليس فحصًا واحدًا بل عائلة. أهمّها في العمل اليومي:
| الخيار | ما يلتقطه |
|---|---|
strictNullChecks |
استخدام قيمة قد تكون null أو undefined |
noImplicitAny |
معاملات ومتغيّرات نوعها any بصمت |
strictFunctionTypes |
أنواع معاملات callback غير آمنة (الدرس 1-2) |
useUnknownInCatchVariables |
معاملة القيمة الملتقَطة على أنها Error دون فحص |
strictPropertyInitialization |
حقول أصناف لا يُسند إليها شيء أبدًا |
منذ TypeScript 6.0، صار strict مفعَّلًا افتراضيًا، فيحصل عليه المشروع الجديد دون أن يطلبه. وفي المشاريع الأقدم يبقى أثمن سطر في الملف. وكل ما تبقّى في هذا الدرس يفترض وجوده.
خياران يتركهما strict
noUncheckedIndexedAccess يجعل الوصول بالفهرس صادقًا. معه يكون lines[0] من نوع Line | undefined، وSTOCK[sku] من نوع number | undefined:
const first = cart.lines[0];
first.sku;
// Error: 'first' is possibly 'undefined'.
const available: number = STOCK[sku];
// Error: Type 'number | undefined' is not assignable to type 'number'.“Cannot read properties of undefined” هو أكثر أخطاء وقت التشغيل شيوعًا في JavaScript، والمصفوفات الفارغة والمفاتيح الناقصة مصادره المفضّلة. وهذا الخيار يلتقطها. والثمن بعض الفحوص الإضافية حيث تعرف أن الفهرس صالح (داخل for (let i = 0; i < xs.length; i++))؛ أما حلقات for…of ودوال المصفوفات مثل map فلا تتأثّر.
exactOptionalPropertyTypes يفصل “الغائبة” عن “الموجودة لكن undefined”. الـ endpoint الخاص بـ PATCH في Cartwheel يعامل { couponCode: undefined } على أنها “أزِل الكوبون” و{} على أنها “اتركه كما هو”. دون الخيار، للاثنين النوع نفسه. ومعه:
type OrderPatch = { couponCode?: string };
const patch: OrderPatch = { couponCode: undefined };
// Error: Type '{ couponCode: undefined; }' is not assignable to type 'OrderPatch'
// with 'exactOptionalPropertyTypes: true'. …إن كنت تقصد فعلًا “قد تكون undefined صراحةً”، فقلها: couponCode?: string | undefined. هذا الخيار أكثر إزعاجًا عند تبنّيه، لأن أنواع مكتبات كثيرة كُتبت دونه، ففعّله بعد الآخرين.
صار tsc --init في TypeScript 5.9 يفعّل الخيارين افتراضيًا في الـ tsconfig الذي يولّده، وهذا تلميح قوي إلى الاتجاه الذي يسير فيه النظام البيئي.
خيارات الوحدات: قل ما تعنيه
verbatimModuleSyntax (TypeScript 5.0) يجعل الـ imports الناتجة مطابقة لما كتبته. أي import دون المعدِّل type يُحفظ؛ والـ imports المعلَّمة بـ type تُمحى. هذا مهم لأن أدوات مثل Vite وesbuild وتجريد الأنواع المدمج في Node تترجم ملفًا واحدًا في كل مرة، ولا تستطيع البحث عمّا إذا كان Order نوعًا:
import { Order } from './orders';
// Error: 'Order' is a type and must be imported using a type-only import
// when 'verbatimModuleSyntax' is enabled.
import { type Order, placeOrder } from './orders'; // finemoduleResolution يخبر TypeScript كيف يجد الـ imports الخاصة بك. طابِقه مع الأداة التي تحلّها فعلًا. لتطبيق يبنيه Vite أو bundler آخر، استخدم "bundler" (مع "module": "esnext" أو "preserve"): يفهم exports في الحزم ولا يتطلّب امتدادات الملفات. وللكود الذي يشغّله Node مباشرة، مثل خادم أو أداة سطر أوامر، استخدم "module": "nodenext"، الذي يتبع قواعد Node الحقيقية، بما فيها امتدادات .js في الـ imports النسبية. الإعداد القديم "node" (المسمّى أيضًا node10) أقدم من exports في الحزم؛ وهو مهمل في TypeScript 6.0 ومُزال في 7.0، مع baseUrl وtarget: "es5".
erasableSyntaxOnly (TypeScript 5.8) يرفض الميزات القليلة في TypeScript التي تولّد كودًا وقت التشغيل: enum، والـ namespaces التي فيها قيم، وخصائص معاملات المُنشئ (parameter properties)، وصيغتَي CommonJS import x = require(…) وexport =. فعّله إن كان كودك يمرّ عبر تجريد الأنواع في Node، أو إن كنت تريد أن يبقى TypeScript “JavaScript مع أنواع”.
بجمعها معًا، يبدو tsconfig لتطبيق مُجمَّع بـ bundler في 2026 هكذا:
{
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"noEmit": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"skipLibCheck": true,
"jsx": "react-jsx"
},
"include": ["src"]
}noEmit لأن الـ bundler هو من ينتج JavaScript وtsc يفحص فقط. وskipLibCheck لأن فحص كل ملف .d.ts في node_modules يكلّف وقتًا ويجد مشكلات لا تستطيع إصلاحها.
تفعيل الخيارات في قاعدة كود قائمة
فعّل خيارًا في قاعدة كود كبيرة وقد تحصل على 2,000 خطأ. لا تُصلحها كلها في pull request واحد. قِس أولًا (tsc --noEmit | grep -c "error TS")، ثم اختر أحد نهجين. إمّا أن تُصلح مجلدًا بعد مجلد باستخدام tsconfig منفصل يمتدّ من الرئيسي ولا يضمّ إلا المجلدات النظيفة، أو أن تفعّل الخيار في كل مكان وتعلّم الأخطاء الموجودة بـ // @ts-expect-error مع مرجع لتذكرة، فيُحاسَب الكود الجديد بالمعيار الجديد فورًا ولا يتحرّك العدد القديم إلا نزولًا.
التمرين سيناريو: ثلاثة أخطاء حقيقية، وتختار أنت الخيارات التي كانت ستلتقطها. بيئة التجربة نفسها تعمل بـ strict ومكتبة ES2022، ولهذا تحتاج بعض الميزات اللاحقة إلى تصريح صغير لتعمل فيها، كما سترى في الدرس القادم.
أهم النقاط
strictعائلة من الفحوص (فحوص null، والـ any الضمني، وفحوص معاملات الدوال وغيرها)، وهو الافتراضي منذ TypeScript 6.0.noUncheckedIndexedAccessيضيفundefinedإلى عمليات البحث في المصفوفات والـ records، فيلتقط أكثر انهيار شيوعًا يفوتهstrict.exactOptionalPropertyTypesيفصل الخاصية الغائبة عن الخاصية المضبوطة صراحةً علىundefined، وهذا مهم لأجسام طلبات PATCH والـ spreads.verbatimModuleSyntaxيجعل الـ imports الناتجة مطابقة لما كتبته، فيجب أن تقول الـ imports الخاصة بالأنواع فقطimport type.- استخدم
moduleResolution: "bundler"للتطبيقات التي يبنيها Vite أو bundler آخر، وnodenextللكود الذي يشغّله Node مباشرة.