EduFlow
أنماط TypeScript المتقدّمة
⁦0%⁩

الوحدة 5 · إيصال TypeScript إلى الإنتاج

صرامة tsconfig التي تؤتي ثمارها

19 دقراءة 4 داختر بحكمة

الدرس في لمحة

اختر خيارات الـ compiler التي تجعل أنواعك جديرة بالثقة، من strict وnoUncheckedIndexedAccess إلى verbatimModuleSyntax وmoduleResolution bundler، وفعّلها في قاعدة كود قائمة دون إيقاف العمل.

  1. 1strict: خط الأساس
  2. 2خياران يتركهما strict
  3. 3خيارات الوحدات: قل ما تعنيه
  4. 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:

TypeScript
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 } على أنها “أزِل الكوبون” و{} على أنها “اتركه كما هو”. دون الخيار، للاثنين النوع نفسه. ومعه:

TypeScript
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 الذي يولّده، وهذا تلميح قوي إلى الاتجاه الذي يسير فيه النظام البيئي.

ثلاث طبقات متراكبة. الطبقة السفلى، strict، هي خط الأساس لفحوص null وany. والطبقة الوسطى تضيف noUncheckedIndexedAccess وexactOptionalPropertyTypes وnoImplicitOverride لصحة أكثر صرامة. والطبقة العليا تغطّي سلوك الوحدات: verbatimModuleSyntax وmoduleResolution bundler وerasableSyntaxOnly. كيف تُولَّد الوحدات وتُحَلّ verbatimModuleSyntax · moduleResolution: bundler · erasableSyntaxOnly صحة تتجاوز strict noUncheckedIndexedAccess · exactOptionalPropertyTypes · noImplicitOverride خط الأساس strict (افتراضي منذ TypeScript 6.0)
ثلاث طبقات لـ tsconfig حديث

خيارات الوحدات: قل ما تعنيه

verbatimModuleSyntax (TypeScript 5.0) يجعل الـ imports الناتجة مطابقة لما كتبته. أي import دون المعدِّل type يُحفظ؛ والـ imports المعلَّمة بـ type تُمحى. هذا مهم لأن أدوات مثل Vite وesbuild وتجريد الأنواع المدمج في Node تترجم ملفًا واحدًا في كل مرة، ولا تستطيع البحث عمّا إذا كان Order نوعًا:

TypeScript
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'; // fine

moduleResolution يخبر 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 هكذا:

tsconfig.json
{
  "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 مباشرة.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

مع تفعيل strict وإيقاف noUncheckedIndexedAccess، ماذا يحدث هنا حين تكون السلة فارغة؟

TypeScript
const first = cart.lines[0];
console.log(first.sku);
السؤال 2 من 4

type Patch = { couponCode?: string }. مع تفعيل exactOptionalPropertyTypes، أيّ قيمة تُرفض؟

السؤال 3 من 4

في ملفك import { Order, placeOrder } from './orders' حيث Order نوع. مع تفعيل verbatimModuleSyntax، ماذا تكتب؟

السؤال 4 من 4

في tsconfig تطبيقك المبني بـ Vite القيمة "moduleResolution": "node". إلى ماذا يجب أن تغيّرها، ولماذا؟

تدريب

اختر بحكمة

أيّ الخيارات كانت ستلتقط هذه الأخطاء؟

نظام الدفع في Cartwheel يُترجم أصلًا مع strict: true. ومع ذلك وصلت هذه الأخطاء الثلاثة إلى الإنتاج في الربع الماضي:

  1. انهيار السلة الفارغة. كان ملخّص الطلب يقرأ cart.lines[0].sku لعرض صورة مصغّرة. ومع سلة فارغة، انهارت الصفحة برسالة Cannot read properties of undefined.
  2. اختفاء الكوبونات. كان نموذج يرسل { couponCode: formValue.coupon } إلى الـ endpoint الخاص بـ PATCH. ونوع formValue.coupon هو string | undefined (فالحقل اختياري)؛ وحين كان الحقل فارغًا، كانت قيمته undefined، والـ API يعامل couponCode الموجودة بقيمة undefined على أنها “أزِل الكوبون”، فخسر العملاء كوبونات كانوا قد طبّقوها سابقًا.
  3. خطأ ضريبي صامت. أُعيدت تسمية PriceCalculator.applyTax() إلى addTax() في الصنف الأساسي. وما زال الصنف الفرعي GulfPriceCalculator يعرّف applyTax()، التي لم تعد تتجاوز شيئًا، فطُبّقت على طلبات الخليج النسبة الافتراضية للضريبة.

اختر كل خيار في الـ compiler كان سيحوّل، إذا أُضيف إلى strict، واحدًا على الأقل من هذه الأخطاء إلى خطأ ترجمة.

أيّ الخيارات كانت ستلتقط هذه الأخطاء؟