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

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

ترحيل JavaScript إلى TypeScript، والحفاظ على السرعة

18 دقراءة 4 درتّب الخطوات

الدرس في لمحة

رحّل قاعدة كود JavaScript إلى TypeScript ملفًا بعد ملف بينما تستمر في نشر التحديثات، وحافظ على سرعة فحص الأنواع مع نموّ قاعدة الكود وأنواعها.

  1. 1التعايش أولًا
  2. 2افحص JavaScript قبل تحويلها
  3. 3حوّل من الأوراق نحو الداخل
  4. 4الحفاظ على السرعة
  5. 5إلى أين من هنا

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

الترحيل الذي قُدته نقل نظام دفع وإدارة طلبات مكتوبًا بـ 400,000 سطر من JavaScript إلى TypeScript صارمة خلال نحو ثمانية عشر شهرًا، دون تجميد للميزات ودون branch طويل العمر. ونشرنا التحديثات كل يوم طوال تلك المدة. تقنيات مستوى الأنواع في هذه الدورة مهمة، لكن في قاعدة كود حقيقية العملية هي ما يقرّر إن كان الترحيل سيكتمل. هذا الدرس هو تلك العملية، إضافة إلى كيفية الحفاظ على سرعة فحص الأنواع بعد أن تنتهي.

التعايش أولًا

ابدأ بالسماح لـ TypeScript وJavaScript بالعيش معًا:

tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "noEmit": true,
    "strict": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "target": "es2022",
    "skipLibCheck": true
  },
  "include": ["src"]
}

allowJs تُدخل ملفات .js إلى البرنامج، فتستطيع ملفات .ts استيرادها والعكس. وnoEmit تترك البناء للـ bundler الموجود؛ وtsc --noEmit يعمل في CI كفاحص فقط. وstrict مفعَّل منذ اليوم الأول، لكنه لا ينطبق إلا على ملفات .ts، فيُحاسَب الكود الجديد بالمعيار الكامل ويُترك القديم وشأنه. ادمج هذا على main في pull request صغير واحد. لا شيء في التطبيق يتغيّر.

افحص JavaScript قبل تحويلها

يمكنك الحصول على فحص حقيقي في ملف .js دون إعادة تسميته. أضف // @ts-check في أعلاه، فيفحص TypeScript الملف، قارئًا الأنواع من تعليقات JSDoc:

src/money.js
// @ts-check

/** @typedef {'USD' | 'EUR' | 'EGP'} Currency */
/** @typedef {{ cents: number; currency: Currency }} Money */

/**
 * @param {Money} a
 * @param {Money} b
 * @returns {Money}
 */
export function add(a, b) {
  if (a.currency !== b.currency) throw new Error('Currency mismatch');
  return { cents: a.cents + b.cents, currency: a.currency };
}

إنه الفاحص نفسه، بالقواعد نفسها، على ملف JavaScript عادي. استخدمه على أكثر الملفات حساسية وخطورة أولًا. ويستطيع JSDoc حتى استيراد أنواع من ملفات .ts بـ @import أو import('./types').Order، فيستخدم ملف JavaScript أنواع المجال التي كتبتها أصلًا.

حوّل من الأوراق نحو الداخل

حين تعيد تسمية الملفات فعلًا، يكون الترتيب مهمًا. ارسم (أو ولّد) مخطّط الـ imports وابدأ من الأوراق: الوحدات التي لا تستورد شيئًا آخر من قاعدة كودك، مثل دوال المال المساعدة، والتنسيق، والثوابت، وأنواع المجال. ثم حوّل الوحدات التي لا تعتمد إلا على وحدات محوَّلة، واستمر في العمل نحو الداخل باتجاه نقاط الدخول.

مخطّط الـ imports في نظام الدفع. صفحة الدفع تستورد عميل الـ API ومخزن السلة، وكلاهما يستورد أداتي المال والتنسيق. الأوراق، أي المال والتنسيق، صارت TypeScript؛ وعميل الـ API ومخزن السلة قيد العمل؛ وصفحة الدفع ما زالت JavaScript. يسير التحويل من الأسفل إلى الأعلى. checkoutPage.js apiClient.ts cartStore.js money.ts format.ts قيد العمل أولًا أخيرًا
حوّل الأوراق أولًا، كي يستورد كل ملف TypeScript جديد كودًا محدَّد الأنواع

تحويل نقطة دخول أولًا يعطيك ملف .ts كل ما يستورده بلا أنواع، أي كثيرًا من any وقليلًا جدًا من الفحص. أما تحويل ورقة أولًا فيعطي كل ملف لاحق أنواعًا حقيقية يبني عليها.

وإلى جانب الأوراق، أعطِ الأولوية لـ الحدود: عميل الـ API، والمحلّلات، ونماذج المجال. Order محدَّد النوع واستجابة API محلَّلة يحميان كل ملف يلمسهما، محوَّلًا كان أم لا. ولهذا تهمّ الأقسام 1 و2 و4 من هذه الدورة في الترحيل أكثر من القسم 3.

حين يختفي آخر ملف .js، أزِل allowJs. ثم شدّ الخيارات الإضافية من صرامة tsconfig التي تؤتي ثمارها تدريجيًا، بدءًا بـ noUncheckedIndexedAccess، واحدًا تلو الآخر، مُخفيًا الأخطاء الموجودة بـ // @ts-expect-error مع تذكرة، كي يبلغ الكود الجديد المستوى المطلوب فورًا.

الحفاظ على السرعة

في قاعدة كود كبيرة، تصبح سرعة فحص الأنواع مشكلة في تجربة المطوّرين. قِس قبل أن تخمّن: tsc --noEmit --extendedDiagnostics يطبع زمن الفحص والذاكرة وعدد تجسيدات الأنواع. القفزة المفاجئة في التجسيدات تأتي دائمًا تقريبًا من أداة ثقيلة على مستوى الأنواع، مثل Paths التعاودي من الأنواع التعاودية ومتى تتوقّف، مطبَّقة على نوع كبير. و--generateTrace <dir> يسجّل أين يذهب الوقت، ملفًا بعد ملف.

الإصلاحات البنيوية راسخة. فعّل incremental كي لا يُعاد فحص الملفات التي لم تتغيّر. وقسّم المستودع الكبير إلى مراجع مشاريع (project references) (composite: true في كل حزمة، وtsc -b في الجذر)، فيُفحص كل جزء مرة واحدة وتُعاد استخدام تصريحاته. وفضّل الـ interfaces التي يمتدّ (extend) بعضها من بعض على السلاسل الطويلة من تقاطعات & لأنواع الكائنات الكبيرة، لأن الـ compiler يخزّن علاقات الـ interfaces مؤقتًا بفعالية أكبر. وعلّق أنواع إرجاع الدوال المُصدَّرة، فهذا يوفّر على الفاحص إعادة استنتاجها ويُبقي مخرجات التصريحات مستقرة.

إلى أين من هنا

استخدمت الأنواع لتصميم الحالات، ووجّهت الـ inference، وكتبت كودًا على مستوى الأنواع باعتدال، وبنيت الأنماط التي تعتمد عليها قواعد الكود الحقيقية، وضبطت الـ compiler ليسندها. والعادة التي يجب أن تحتفظ بها هي تلك التي من الدرس الأول: قبل أن تكتب type، سمِّ الخطأ الذي يمنعه. ثم اختر أبسط type يمنعه. التمرين أدناه يرتّب دليل الترحيل؛ والتقييم النهائي يغطّي الدورة كلها.

أهم النقاط

  • رحّل تدريجيًا باستخدام allowJs، كي تعيش ملفات JavaScript وTypeScript جنبًا إلى جنب ويُنشر التطبيق كل يوم.
  • // @ts-check مع أنواع JSDoc يعطي ملف JavaScript فحصًا حقيقيًا قبل أن تعيد تسميته.
  • حوّل وحدات الأوراق أولًا واعمل نحو نقاط الدخول، كي يستورد كل ملف محوَّل حديثًا كودًا محدَّد الأنواع.
  • حدّد أنواع الحدود (استجابات الـ API ونماذج المجال) مبكرًا؛ فهي تعطي أكبر حماية مقابل كل ساعة عمل.
  • قِس فحص الأنواع بـ --extendedDiagnostics، واستخدم البناء التدريجي ومراجع المشاريع، وأبقِ كود مستوى الأنواع الثقيل خارج المسارات الساخنة.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

تبدأ ترحيل تطبيق JavaScript من 1,500 ملف. أيّ خطوة أولى تُبقي التطبيق قابلًا للنشر؟

السؤال 2 من 4

بأيّ ترتيب يجب أن تحوّل الوحدات؟

السؤال 3 من 4

يُظهر tsc --extendedDiagnostics تسعة ملايين تجسيد للأنواع، معظمها من ملف واحد. ما السبب المرجّح؟

السؤال 4 من 4

كيف يساعد // @ts-check أعلى ملف .js في الترحيل؟

تدريب

رتّب الخطوات

رتّب دليل الترحيل

تطبيق إدارة الطلبات في Cartwheel مكوَّن من 1,500 ملف JavaScript ويجب أن يستمر في نشر التحديثات كل يوم. رتّب خطوات الترحيل هذه بالترتيب الذي يوصي به الدرس، من أول pull request إلى آخره.

استخدم أزرار الأسهم لإعادة الترتيب.

  1. 1أضف // @ts-check وأنواع JSDoc إلى ملفات عميل الـ API ونماذج المجال
  2. 2حوّل الوحدات التي تعتمد على وحدات محوَّلة أصلًا، عاملًا نحو نقاط الدخول
  3. 3أعِد تسمية وحدات الأوراق (الدوال المساعدة التي لا تستورد شيئًا داخليًا) من .js إلى .ts
  4. 4أزِل allowJs حين لا يبقى أي ملف مصدر .js
  5. 5أضف tsconfig فيه allowJs وnoEmit وstrict، وشغّل tsc --noEmit في CI بجوار البناء الموجود
  6. 6فعّل noUncheckedIndexedAccess والخيارات الإضافية الأخرى واحدًا تلو الآخر، معلّمًا الأخطاء القديمة بـ @ts-expect-error