اذهب إلى المحتوى

الخطوات الأولى في بناء تطبيقات الويب باستعمال TypeScript


ابراهيم الخضور

بعد المقدمة الموجزة التي تحدثنا فيها عن أساسيات اللغة TypeScript في المقال السابق، سنكون جاهزين لبدء رحلتنا لنصبح مطورين شاملين لتطبيقات الويب باستخدام TypeScript. سنركز في هذا القسم على المشاكل التي قد تنشأ عن استخدام اللغة لتطوير تطبيقات express للواجهة الخلفية وReact للواجهة الأمامية، بدلًا من تقديم مدخل موّسع عن كل جوانب اللغة. وسنركز أيضًا على استخدام أدوات التطوير المتاحة بالإضافة إلى الميزات التي تقدمها اللغة.

تجهيز كل شيء لبدء العمل

ثَبّت ما تحتاجه لاستخدام TypeScript على المحرر الذي تختاره، ستحتاج إلى تثبيت الموسِّع typescript hero إن كنت ستعمل على Visual Studio Code.

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

نقصد أحيانًا بالحاجة إلى التصريف في بيئات الإنتاج، أن نوجد "خطوة لبناء التطبيق". حيث تُصرّف كل شيفرة TypeScript خلال خطوة بناء التطبيق إلى شيفرة JavaScript ضمن مجلد منفصل، ومن ثم تُنفِّذ بيئة الإنتاج الشيفرة الموجودة ضمن هذا المجلد. يُفضَّل في بيئات الإنتاج أن نستخدم التصريف في الزمن الحقيقي، وإعادة التحميل التلقائي لنتمكن من متابعة النتائج بشكل أسرع.

لنبدأ بكتابة أول تطبيق لنا باستخدام TypeScript. ولتبسيط الأمر، سنستخدم حزمة npm تُدعى ts-node. حيث تصرّف هذه المكتبة ملف TypeScript وتنفذه مباشرة. وبالتالي لا حاجة لخطوة تصريف مستقلة.

يمكن تثبيت المكتبة ts-node والحزمة الرسمية للغة TypeScript لكافة المشاريع globally كالتالي:

npm install -g ts-node typescript

إن لم تستطع أو لم ترد تثبيت حزم شاملة، يمكنك إنشاء مشروع npm وتثبت فيه الاعتماديات اللازمة فقط ومن ثم تُنفِّذ شيفرتك ضمنه. كما سنستخدم المقاربة التي اتبعناها في القسم 3، حيث نهيئ مشروع npm بتنفيذ الأمر npm init في مجلد فارغ، ونثبّت بعدها الاعتماديات بتنفيذ الأمر:

npm install --save-dev ts-node typescript

كما يجب أن نهيئ تعليمة scripts ضمن الملف package.json كالتالي:

{
  // ..
  "scripts": {
    "ts-node": "ts-node"  },
  // ..
}

يمكنك استخدام ts-node ضمن المجلد بتنفيذ الأمر npm run ts-node. وتجدر الملاحظة أن استخدام ts-node من خلال الملف package.json يقتضي أن تبدأ كل أسطر الأوامر بالرمز "--". لذا سيكون عليك تنفيذ الأمر التالي إن أردت مثلًا تنفيذ الملف file.ts باستخدام ts-node:

npm run ts-node -- file.ts

نشير أيضًا إلى وجود أرضية عمل خاصة باللغة TypeScript على الإنترنت، حيث يمكنك تجريب شيفرتك والحصول على شيفرة JavaScript الناتجة وأخطاء التصريف المحتملة بسرعة.

ملاحظة: يمكن أن تتضمن أرضية العمل قواعد تهيئة مختلفة (وهذا ما سنوضحه لاحقًا) عما هو موجود في بيئة التطوير الخاصة بك، لهذا قد تجد تحذيرات مختلفة. يمكن تعديل قواعد التهيئة لأرضية العمل هذه من خلال القائمة المنسدلة config.

ملاحظة عن أسلوب كتابة الشيفرة

إن لغة JavaScript بحد ذاتها مريحة الاستخدام، ويمكنك أن تنفذ ما تريده بطرق مختلفة في أغلب الأحيان. فعلى سبيل المثال يمكنك استخدام الدوال named، أو anynamous، وقد تستخدم const، أو var، أو let للتصريح عن متغير، وكذلك الاستخدامات المختلفة للفاصلة المنقوطة. سيختلف هذا القسم عن البقية بطريقة استخدام الفاصلة المنقوطة. ولا يمثل هذا الاختلاف طريقة خاصة باللغة لكنه أكثر ما يكون أسلوبًا لكتابة شيفرة أي نوع من JavaScript. واستخدام هذا التنسيق أو عدمه سيعود كليًا للمبرمج. لكن من المتوقع أن يغير أسلوبه في كتابة الشيفرة لتتماشى مع الأسلوب المتبع في هذا القسم، إذ يجب إنجاز تمارين القسم مستخدمًا الفواصل المنقوطة ومراعيًا أسلوب كتابة الشيفرة المستخدم فيه. كما ستجد اختلافًا في تنسيق الشيفرة في هذا القسم بالمقارنة مع التنسيق الذي اتبعناه في بقية أقسام المنهاج كتسمية المجلدات مثلًا.

لنبدأ بإنشاء دالة بسيطة لتنفيذ عملية الضرب. ستبدو تمامًا كما لو أنها مكتوبة بلغة JavaScript:

const multiplicator = (a, b, printText) => {
  console.log(printText,  a * b);
}

multiplicator(2, 4, 'Multiplied numbers 2 and 4, the result is:');

وكما نرى، فالدالة مكتوبة بشيفرة JavaScript عادية دون استخدام ميزات TS. ستصرّف وتنفذ الشيفرة بلا مشاكل باستخدام الأمر npm run ts-node -- multiplier. لكن ما الذي سيحدث إن مررنا نوعًا خاطئًا من المعاملات إلى الدالة؟ لنجرب ذلك!

const multiplicator = (a, b, printText) => {
  console.log(printText,  a * b);
}

multiplicator('how about a string?', 4, 'Multiplied a string and 4, the result is:');

عند تنفيذ الشيفرة، سيكون الخرج:

 "Multiplied a string and 4, the result is: NaN".

أليس من الجيد أن تمنعنا اللغة من ارتكاب هذا الخطأ؟ سترينا هذه المشكلة أولى مزايا TS. لنعرف أنواعًا للمعاملات ونرى ما الذي سيتغير.

تدعم اللغة TS أنواعًا مختلفة، مثل: number، وstring، وArray. يمكنك إيجاد قائمة بالأنواع في توثيق TS على الانترنت. كما يمكن إنشاء أنواع مخصصة أكثر تعقيدًا.

إنّ أول معاملين لدالة الضرب من النوع number والثالث من النوع string.

const multiplicator = (a: number, b: number, printText: string) => {
  console.log(printText,  a * b);
}

multiplicator('how about a string?', 4, 'Multiplied a string and 4, the result is:');

لم تعد الشيفرة السابقة شيفرة JavaScript صالحة للتنفيذ. فلو حاولنا تنفيذها، لن تُصرَّف:

code_no_compile_01.png

إنّ إحدى أفضل ميزات محرر TS، أنك لن تحتاج إلى تنفيذ الشيفرة حتى ترى المشكلة. فالإضافة VSCode فعالة جدًا، حيث ستبلغك مباشرة إن حاولت استخدام النوع الخاطئ.

vscode_warning_02.png

إنشاء أول نوع خاص بك

لنطور دالة الضرب إلى آلة حاسبة تدعم أيضًا الجمع والقسمة. ينبغي أن تقبل الآلة الحاسبة ثلاث معاملات: عددين وعملية قد تكون الضرب multiply، أو الجمع add، أو القسمة divide.

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

يمكن إنشاء نوع باستخدام التعليمة الداخلية type في TS. سنصف فيما سيأتي النوع Operation الذي عرًفناه:

type Operation = 'multiply' | 'add' | 'divide';

يقبل النوع Op ثلاثة أشكال من الدخل وهي تمامًا القيم النصية الثلاثة التي نريد. يمكننا باستخدام العامل OR ('|') تعريف متغير يقبل قيمًا متعددة وذلك بإنشاء نوع موحَّد union type.لقد استخدمنا في الحالة السابقة القيمة النصية بحرفيتها (وهذا ما يدعى التعريف المختصر للنوع النصي string literal types)، لكن باستخدام الأنواع الموحَّدة، ستجعل المصرِّف قادرًا على قبول قيمة نصية أو عددية: string | number.

تُعرّف التعليمة type اسمًا جديدًا للنوع أو ما يسمى اسمًا مستعارًا للنوع. وطالما أن النوع المُعرَّف هو اتحاد بين ثلاث قيم مقبولة، فمن المفيد إعطاء النوع اسمًا مستعارًا يصفه بشكل جيد.

لنلق نظرة على الآلة الحاسبة:

type Operation = 'multiply' | 'add' | 'divide';

const calculator = (a: number, b: number, op : Operation) => {
  if (op === 'multiply') {
    return a * b;
  } else if (op === 'add') {
    return a + b;
  } else if (op === 'divide') {
    if (b === 0) return 'can\'t divide by 0!';
    return a / b;
  }
}

ستلاحظ الآن أنك لو مررت الفأرة فوق النوع Operation في المحرر، ستظهر لك مباشرة اقتراحات عن طريقة استخدامه:

type_use_suggestione_03.png

ولو أردنا استخدام قيمة غير موجودة ضمن النوع Operation، ستظهر لك مباشرة إشارة التحذير الحمراء ومعلومات إضافية ضمن المحرر:

type_mismatch_warn_04.png

لقد أبلينا جيدًا حتى اللحظة، لكننا لم نطبع بعد النتيجة التي تعيدها دالة الآلة الحاسبة. غالبًا ما نريد معرفة النتيجة التي تعيدها الدالة، ومن الأفضل أن نضمن أن الدالة ستعيد القيمة التي يفترض أن تعيدها. لنضف إذًا القيمة المعادة number إلى الدالة:

type Operation = 'multiply' | 'add' | 'divide';

const calculator = (a: number, b: number, op: Operation): number => {

  if (op === 'multiply') {
    return a * b;
  } else if (op === 'add') {
    return a + b;
  } else if (op === 'divide') {
    if (b === 0) return 'this cannot be done';
    return a / b;
  }
}

سيعترض المصرِّف مباشرة على فعلتنا، ذلك أن الدالة ستعيد في إحدى الحالات (القسمة على صفر) قيمة نصية. هناك عدة طرق لحل المشكلة. إذ يمكن إعادة تعريف النوع المعاد ليتضمن القيم النصية string على النحو التالي:

const calculator = (a: number, b: number, op: Operation): number | string =>  {
  // ...
}

أو قد ننشئ نوعًا جديدًا للقيم المعادة يتضمن النوعين اللذين تعيدهما الدالة تمامًا مثل النوع Operation.

type Result = string | number

const calculator = (a: number, b: number, op: Operation): Result =>  {
  // ...
}

لكن السؤال الذي يطرح نفسه هو: هل من الطبيعي لدالة أن تعيد قيمة نصة؟

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

وعليك أن تدرك أمرًا آخر، فحتى لو عرّفنا أنواعًا لمعاملاتنا، فإن شيفرة JavaScript المتولدة عند التصريف لن تكون قادرة على التحقق من هذه الأنواع أثناء التنفيذ. وإن كانت قيمة المعامل الذي يحدد العملية قادمةً من مكتبة خارجية على سبيل المثال، فلن تضمن أنها واحدة من القيم المسموحة. لذلك من الأفضل أن تنشئ معالجات للأخطاء، وأن تكون منتبهًا لحدوث ما لا تتوقعه. وفي الحالة التي تصادفك فيها عدة قيم مقبولة بينما ينبغي رفض بقية القيم وإلقاء خطأ، ستجد أن كتلة switch…case أكثر ملائمة من كتلة "if….else".

ستبدو شيفرة الآلة الحاسبة التي طورناها على النحو التالي:

type Operation = 'multiply' | 'add' | 'divide';

type Result = number;

const calculator = (a: number, b: number, op : Operation) : Result => {
  switch(op) {
    case 'multiply':
      return a * b;
    case 'divide':
      if( b === 0) throw new Error('Can\'t divide by 0!');
      return a / b;
    case 'add':
      return a + b;
    default:
      throw new Error('Operation is not multiply, add or divide!');
  }
}

try {
  console.log(calculator(1, 5 , 'divide'))
} catch (e) {
  console.log('Something went wrong, error message: ', e.message);
}

لا بأس بما فعلنا حتى الآن، لكن من الأفضل لو استخدمنا سطر الأوامر لإدخال القيم بدلًا من تغييرها ضمن الشيفرة في كل مرة. سنجرب ذلك كما لو كنا ننفذ تطبيق Node نظامي، باستخدام process.argv. لكن مشكلة ستقع:

cpmmand_line_node_problem_05.png

الحصول على الأنواع في حزم npm من خلال Type@

لنعد إلى الفكرة الأساسية للغة TS. تتوقع اللغة أن تكون كل الشيفرة المستخدمة قادرة على تمييز الأنواع، كما تتوقع ذلك من شيفرة مشروعك الخاص الذي هيِّئ بشكل مناسب. ولا تتضمن المكتبة TS بذاتها سوى الأنواع الموجودة في الحزمة TS. من الممكن كتابة أنواع خاصة بك لمكتبة، لكن ذلك غير ضروري إطلاقًا! فمجتمع TS قد فعل ذلك بالنيابة عنّا.

وكما هو حال npm، يرحب عالم TS بالشيفرة مفتوحة المصدر. ومجتمع TS مجتمع نشط ويتفاعل دومًا مع آخر التحديثات والتغييرات في حزم npm المستخدمة أكثر.

ستجد غالبًا كل ماتحتاجه من أنواع حزم npm، فلا حاجة لإنشاء هذه الأنواع بنفسك لكل اعتمادية قد تستخدمها.

عادة ما نحصل على الأنواع المعرّفة في الحزم الموجودة من خلال منظمة Types@ وباستخدام npm. إذ يمكنك إضافة الأنواع الموجودة في حزمة إلى مشروعك بتثبيت حزمة npm لها نفس اسم حزمتك لكنه مسبوق بالبادئة Types@. فعلى سبيل المثال ستُثبَّت الأنواع التي توفرها المكتبة express بتنفيذ الأمر npm install --save-dev @types/express. تجري صيانة وتطوير Types@ من قبل Definitely typed، وهو مشروع مجتمعي يهدف إلى صيانة أنواع كل ما هو موجود في مكان واحد.

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

اقتباس

ملاحظة: لاحاجة لتمييز الأنواع في نسخة الإنتاج طالما أن تمييز الأنواع سيجري قبل التصريف. لذلك ينبغي أن يبقى تعريف الأنواع في قسم اعتماديات التطوير داخل ملف package.json.

طالما أن المتغير العام process قد عُرِّف من قبل Node، سنحصل على الأنواع التي يؤمنها بتثبيت الحزمة types/node@:

npm install --save-dev @types/node

لن يعترض المصرِّف على المتغير process بعد تثبيت الأنواع. ولاحظ أنه لا حاجة لطلب الأنواع ضمن الشيفرة، إذ يكفي تثبيت الحزمة التي تُعرِّفها.

تحسينات على المشروع

سنضيف تاليًا سيكربت npm لتشغيل كلا من برنامج دالة الضرب، وبرنامج الآلة الحاسبة:

{
  "name": "part1",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "ts-node": "ts-node",
    "multiply": "ts-node multiplier.ts",    "calculate": "ts-node calculator.ts"  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "ts-node": "^8.6.2",
    "typescript": "^3.8.2"
  }
}

سنجعل دالة الضرب تعمل من خلال سطر الأوامر عند إجراء التعديلات التالية:

const multiplicator = (a: number, b: number, printText: string) => {
  console.log(printText,  a * b);
}

const a: number = Number(process.argv[2])
const b: number = Number(process.argv[3])
multiplicator(a, b, `Multiplied ${a} and ${b}, the result is:`);

وسننفذ البرنامج كالتالي:

npm run multiply 5 2

لو نفذنا البرنامج بمعاملات ليست من النوع الصحيح، كما تُظهر الشيفرة التالية:

npm run multiply 5 lol

سيعمل البرنامج أيضًا معطيًا الخرج التالي:

Multiplied 5 and NaN, the result is: NaN

والسبب في ذلك أن تنفيذ الأمر ('Number('lol سيعيد القيمة NaN وهذه القيمة من النوع number. لهذا لا يمكن أن تنقذنا TS من مأزق كهذا.

لا بدّ في هذه الحالات من تقييم البيانات التي نحصل عليها من سطر الأوامر لكي نمنع سلوكًا كهذا.

ستبدو النسخة المحسّنة من "دالة الضرب" كالتالي:

interface MultiplyValues {
  value1: number;
  value2: number;
}

const parseArguments = (args: Array<string>): MultiplyValues => {
  if (args.length < 4) throw new Error('Not enough arguments');
  if (args.length > 4) throw new Error('Too many arguments');

  if (!isNaN(Number(args[2])) && !isNaN(Number(args[3]))) {
    return {
      value1: Number(args[2]),
      value2: Number(args[3])
    }
  } else {
    throw new Error('Provided values were not numbers!');
  }
}

const multiplicator = (a: number, b: number, printText: string) => {
  console.log(printText,  a * b);
}

try {
  const { value1, value2 } = parseArguments(process.argv);
  multiplicator(value1, value2, `Multiplied ${value1} and ${value2}, the result is:`);
} catch (e) {
  console.log('Error, something bad happened, message: ', e.message);
}

عندما ننفذ البرنامج الآن:

npm run multiply 1 lol

سنحصل على رسالة خطأ مناسبة للوضع:

Error, something bad happened, message:  Provided values were not numbers!

يحتوي تعرف الدالة parseArguments بعض الأشياء الملفتة:

const parseArguments = (args: Array<string>): MultiplyValues => {
  // ...
}

سنجد أولًا أنّ المعامل arg هو مصفوفة من النوع "string"، وأنّ القيم المعادة من النوع "MultiplayValues" الذي عُرِّف بالشكل التالي:

interface MultiplyValues {
  value1: number;
  value2: number;
}

لقد استخدمنا في تعريف هذا النوع التعليمة Interface للغة TS، وهي إحدى الطرق المتبعة لتحديد شكل الكائن. إذ ينبغي كما هو واضح في حالتنا، أن تكون القيمة المعادة كائنًا له خاصيتين value1 وvalue2 من النوع "number".

التمارين 9.1 - 9.3

التثبيت

سننجز التمارين من 9.1 إلى 9.7 ضمن مشروع Node واحد. أنشئ المشروع في مجلد فارغ مستخدمًا الأمر npm init وثبِّت حزمتي ts-node وtypescript. أنشئ أيضًا ملفًا باسم tsconfig.json ضمن المجلد بحيث يحتوي الشيفرة التالية:

{
  "compilerOptions": {
    "noImplicitAny": true,
  }
}

يستخدم هذا الملف لتحديد الطريقة التي سيفسر بها مصرِّف TS الشيفرة المكتوبة، وما هو مقدار التشدد الذي سيفرضه المصرِّف، وما هي الملفات التي ينبغي مراقبتها أو تجاهلها، والكثير الكثير من الأشياء. سنستخدم حاليًا خيار المصرِّف noImplicitAny الذي يشترط أن تحمل جميع المتغيرات أنواعًا محددة.

9.1 مؤشر كتلة الجسم

ضع شيفرة هذا التمرين في الملف bmiCalculator.ts.

اكتب دالة باسم calculateBmi تحسب مؤشر كتلة الجسم BMI بناء على طول محدد (سنتيمتر) ووزن محدد (كيلو غرام)، ثم أعد رسالة مناسبة تحمل النتيجة.

استدعي الدالة من شيفرتك ومرر إليها معاملات جاهزة ثم اطبع النتيجة.

ينبغي أن تطبع الشيفرة التالية:

console.log(calculateBmi(180, 74))

النتيجة التالية:

Normal (healthy weight)

أنشئ سكربت npm لكي تنفذ البرنامج باستخدام الأمر npm run calculateBmi

9.2 آلة حاسبة لساعات التمرين

ضع شيفرة هذا التمرين في الملف exerciseCalculatore.ts.

اكتب دالة باسم calculateExercise تحسب متوسط ساعات التمرين اليومية وتقارنها بعدد الساعات التي ينبغي الوصول إليها يوميًا، ثم تعيد كائنًا يتضمن القيم التالية:

  • عدد الأيام
  • عدد أيام التمرين
  • القيمة المستهدفة أساسًا
  • متوسط الوقت المحسوب
  • قيمة منطقية تحدد إن تم تحقيق الهدف أم لا.
  • تقييمًا بين 1-3 يصف حسن التمرين خلال ساعات التمرين. قرر أسلوب التقييم كما تشاء.
  • قيمة نصية تصف التقييم

تمرر ساعات التمرين اليومية إلى الدالة مثلل مصفوفة تحتوي على عدد ساعات التمرين كل يوم خلال فترة التمرين. فلو فرضنا أن ساعات التمرين خلال أسبوع موزعة كالتالي: يوم الاثنين 3، يوم الثلاثاء 0، يوم الأربعاء 2، يوم الخميس 4.5 وهكذا، ستكون المصفوفة مشابهة للمصفوفة التالية:

[3, 0, 2, 4.5, 0, 3, 1]

عليك أن تنشئ واجهة interface من أجل توصيف الكائن الذي سيحمل النتيجة.

لو استدعيت الدالة وقد مررت لها المصفوفة [3, 0, 2, 4.5, 0, 3, 1] والقيمة 2 للمعامل الآخر ستكون النتيجة على النحو:

{ periodLength: 7,
  trainingDays: 5,
  success: false,
  rating: 2,
  ratingDescription: 'not too bad but could be better',
  target: 2,
  average: 1.9285714285714286 }

أنشئ سكربت npm لينفذ الأمر npm run calculateExercise الذي يستدعي الدالة بمعاملات قيمها موجودة مسبقًا في الشيفرة.

9.3 سطر الأوامر

عدّل التمرينين السابقين بحيث يمكنك تمرير قيم معاملات الدالتين calculateBmi وcalculateExercise من خلال سطر الأوامر.

يمكن أن تنفذ برنامجك على سبيل المثال على النحو التالي:

$ npm run calculateBmi 180 91

Overweight

أو على النحو:

$ npm run calculateExercises 2 1 0 2 4.5 0 3 1 0 4

{ periodLength: 9,
  trainingDays: 6,
  success: false,
  rating: 2,
  ratingDescription: 'not too bad but could be better',
  target: 2,
  average: 1.7222222222222223 }

وانتبه إلى أنّ المعامل الأول للدالة السابقة هو القيمة المستهدفة.

تعامل مع الاستثناءات والأخطاء بطريقة مناسبة. وانتبه إلى أنّ الدالة exerciseCalculator ستقبل قيمًا لمعاملاتها بأطوال مختلفة. قرر بنفسك كيف ستجمّع كل البيانات المطلوبة.

المزيد حول قواعد تهيئة TS

لقد استخدمنا في التمارين السابقة قاعدة تهيئة واحدة هي noImplicitAny. وهي بالفعل نقطة انطلاق جيدة، لكن يجب أن نطلع بشيء من التفصيل على الملف "config".

يحتوي الملف tsconfig.json كل التفاصيل الجوهرية التي تحدد الطريقة التي ستنفذ بها TS مشروعك. فيمكنك أن تحدد مقدار التشدد في تفحص الشيفرة، وأن تحدد الملفات التي ستدرجها في المشروع والتي ستستثنيها (يستثنى الملف "node_modules" افتراضيًا)، وأين ستخزّن الملفات المصرِّفة (سنتحدث أكثر عن هذا الموضوع لاحقًا).

لنضع القواعد التالية في الملف tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true
  }
}

لا تقلق بخصوص القواعد في الجزء compilerOptions من الملف، فسنمر عليها بشيئ من التفصيل في القسم 2.

يمكنك أن تجد شروحات عن كل قاعدة تهيئة من خلال توثيق TS، أو من خلال صفحة الويب tsconfig page، أو من خلال تعريف تخطيط الملف tsconfig والذي صيغ لسوء الحظ بطريقة أقل وضوحًا من الخيارين السابقين.

إضافة المكتبة express إلى الخلطة

وصلنا إلى مرحلة لا بأس بها. فمشروعنا قد ثّبت، ويحتوي على نسخة قابلة للتنفيذ من الآلة الحاسبة. لكن طالما أنا نهتم بالتطوير الشامل لتطبيقات الويب، فقد حان الوقت لنعمل مع بعض طلبات HTTP.

لنبدأ بتثبيت المكتبة express:

npm install express

ثم سنضيف سكربت start إلى الملف package.json

{
  // ..
  "scripts": {
    "ts-node": "ts-node",
    "multiply": "ts-node multiplier.ts",
    "calculate": "ts-node calculator.ts",
    "start": "ts-node index.ts"  },
  // ..
}

سنتمكن الآن من إنشاء الملف index.ts، ثم سنكتب ضمنه طلب HTTP-GET للتحقق من الاتصال بالخادم:

const express = require('express');
const app = express();

app.get('/ping', (req, res) => {
  res.send('pong');
});

const PORT = 3003;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

يبدو أن كل شيء يعمل على ما يرام، لكن كما هو متوقع، لا بدّ من تحديد نوع كل من المعاملين req وres للدالة app.get (لأننا وضعنا قاعدة تهيئة تفرض أن يكون لكل متغير نوع). لو نظرت جيدًا، ستجد أن VSCode سيعترض على إدراج express. ستجد خطًا منقطا أصفر اللون تحت التعليمة require. وعندما نمرر الفأرة فوق الخطأ ستظهر الرسالة التالية:

vscode_express_import_propblem_06.png

إنّ سبب الاعتراض هو أن require يمكن أن تُحوَّل إلى import. لننفذ النصيحة ونستخدم import كالتالي:

import express from 'express';

ملاحظة: تقدم لك VSCode ميزة إصلاح المشاكل تلقائيًا بالنقر على زر "…Quick fix". انتبه إلى تلك الاصلاحات التلقائية، وحاول أن تتابع النصائح التي يقدمها محرر الشيفرة، لأن ذلك سيحسّن من شيفرتك ويجعلها أسهل قراءة. كما يمكن أن يكون الإصلاح التلقائي للأخطاء عاملًا رئيسيًا في توفير الوقت.

سنواجه الآن مشكلة جديدة. سيعترض المصرِّف على عبارة import. وكالعادة يمثل المحرر المكان الأفضل لإيجاد حلول للمشاكل:

compiler_error_using_import_07.png

لم نثبّت أنواع express كما يشير المحرر، لنثبتها إذًا:

npm install --save-dev @types/express

وهكذا سيعمل البرنامج بلا أخطاء.

فلو مررنا الفأرة على عبارة require سنرى أن المصرِّف قد فسّر كل ما يتعلق بالمكتبة express على أنه من النوع "any".

compiler_require_type_issue_08.png

لكن عند استخدام imports فسيعرف المصرِّف الأنواع الفعلية للمتغيرات:

compiler_imports_type_rec_09.png

يعتمد استخدام عبارة الإدراج على أسلوب التصدير الذي تعتمده الحزمة التي ندرجها. وكقاعدة أساسية: حاول أن تدرج وحدات الشيفرة باستخدام التعليمة import أولًا. سنستخدم دائمًا هذا الأسلوب عند كتابة شيفرة الواجهة الأمامية. فإن لم تنجح التعليمة import، جرّب طريقة مختلطة بتنفيذ الأمر ('...')import...= require. كما نوصيك بشدة أن تطلع أكثر على وحدات TS.

لا تزال هنالك مشكلة عالقة:

ban_unused_param_10.png

يحدث هذا لأننا منعنا وجود معاملات غير مستخدمة عند كتابة قواعد التهيئة في الملف tsconfig.json.

{
  "compilerOptions": {
    "target": "ES2020",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true
  }
}

ستسبب لك التهيئة مشاكلًا إن استخدمت دوال معرّفة مسبقُا لمكتبة وتتطلب تصريحًا عن متغير حتى لو لم يستخدم مطلقًا كالحالة التي تواجهنا. ولحسن الحظ فقد حُلًت هذه المشكلة على مستوى قواعد التهيئة. وبمجرد تمرير مؤشر الفأرة فوق الشيفرة التي سببت المشكلة سيعطينا فكرة الحل. وسننجز الحل هذه المرة بالنقر على زر الإصلاح السريع.

unused_variable_issue_solution_11.png

إن كان من المستحيل التخلص من المتغيرات غير المستخدمة، يمكنك أن تضيف إليها البادئة (_) لإبلاغ المصرِّف بأنك قد فكرت بحل ولم تصل إلى نتيجة!

لنعدّل اسم المتغير req ليصبح req_.

وأخيرًا سنكون مستعدين لتشغيل البرنامج. ويبدو أن كل شيء على مايرام.

program_on_run_12.png

ولتبسيط عملية التطوير، ينبغي علينا تمكين ميزة إعادة التحميل التلقائي، وذلك لتحسين انسيابية العمل. لقد استخدمنا سابقًا المكتبة nodemon التي توفر هذه الميزة، لكن البديل عنها في ts-node هي المكتبة ts-node-dev. وقد جرى تصميم هذه الأخيرة لتُستخدم فقط في بيئة التطوير، حيث تتولى أمور إعادة التصريف عند كل تغيير، وبالتالي لا حاجة لإعادة تشغيل التطبيق.

لنثبّت المكتبة ts-node-dev كاعتمادية تطوير:

npm install --save-dev ts-node-dev

ثم علينا إضافة سكربت خاص بها ضمن الملف package.json.

{
  // ...
  "scripts": {
      // ...
      "dev": "ts-node-dev index.ts",  },
  // ...
}

وهكذا ستحصل على بيئة تطوير تدعم إعادة التحميل التلقائي للمشروع بتنفيذ الأمر npm run dev.

التمرينان 9.4 - 9.5

9.4 استخدام المكتبة Express

أضف المكتبة express إلى اعتماديات التطبيق، ثم أنشئ وصلة تخديم endpoint لطلبات HTTP-GET تدعى hello تجيب على الطلب بالعبارة 'Hello FullStack'.

ينبغي أن تُشغّل التطبيق باستخدام الأمر npm start وذلك في بيئة الإنتاج وبالأمر npm run dev في بيئة التطوير التي من المفترض أن تستخدم المكتبة ts-node-dev لتشغيل التطبيق.

استبدل محتوى الملف tsconfig.json بالمحتوى التالي:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
  }
}

تأكد من عدم وجود أية أخطاء!

9.5 تطبيق ويب لقياس مؤشر الكتلة BMI

أضف وصلة تخديم لحاسبة مؤشر الكتلة يمكن استخدامها بإرسال طلبات HTTP-GET إلى وصلة التخديم bmi، بحيث يكون الدخل على شكل معاملات استعلام نصية (query string parameters). فلو أردت مثلًا الحصول على مؤشر كتلة شخص طوله 180 ووزنه 72، ستحصل عليه بطلب العنوان http://localhost:3002/bmi?height=180&weight=72.

ستكون الاستجابة بيانات json لها الشكل التالي:

{
  weight: 72,
  height: 180,
  bmi: "Normal (healthy weight)"
}

اطلع على توثيق express لتعرف آلية الوصول إلى بارامترات الاستعلام.

إن كانت معاملات الطلب من نوع خاطئ أو مفقودة، أجب برمز حالة مناسب ورسالة الخطأ التالية:

{
  error: "malformatted parameters"
}

لا تنسخ شيفرة الحاسبة إلى الملف index.ts، بل اجعلها وحدة TS مستقلة يمكن إدراجها ضمن هذا الملف.

كوابيس استخدام النوع any

قد نلاحظ بعد أن أكملنا تنفيذ أول وصلة تخديم أننا بالكاد استخدمنا TS في هذا الأمثلة الصغيرة. وقد نلاحظ أيضًا بعض النقاط الخطرة عند تفحص الشيفرة بعناية.

لنضف وصلة تخديم طلبات HTTP-GET تدعى calculate إلى تطبيقنا:

import { calculator } from './calculator'

// ...

app.get('/calculate', (req, res) => {
  const { value1, value2, op } = req.query

  const result = calculator(value1, value2, op)
  res.send(result);
});

عندما نمرر مؤشر الفأرة فوق الدالة calculate، ستجد أنها تحمل نوعًا على الرغم من أن الشيفرة ذاتها لا تتضمن أية أنواع.

typing_without_types_13.png

وبتمرير مؤشر الفأرة فوق القيم المستخلصة من الطلب، ستظهر مشكلة جديدة:

variables_type_any_issue_14.png

ستحمل جميع المتغيرات النوع any. لن نتفاجأ كثيرًا بما حدث، لأننا في الواقع لم نحدد نوعًا لأي قيمة منها. تُحل المشكلة بطرق عدة، لكن علينا أولًا أن نفهم سبب حدوث هذا السلوك وما الذي جعل القيم من النوع any.

كل متغير في TS لا يملك نوعًا ولا يمكن الاستدلال على نوعه، سيعتبر من النوع any ضمنًا. وهذا النوع هو بمثابة نوع "بديل" ويعني حرفيًا "أيًا كان النوع". ويحدث هذا الأمر كثيرًا عندما ينسى المبرمج أن يحدد نوعًا للقيم التي تعيدها الدالة ولمعاملاتها.

كما يمكن أن نصُرّح بأن نوع المتغيّر هو any. والفرق الوحيد بين التصريح عن هذا النوع أو تضمينه هو في مظهر الشيفرة المكتوبة، فلن يكترث المصرِّف لأي فرق.

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

وهذا هو السبب في وجود قاعدة التهيئة noImplicitAny على مستوى المصرِّف. ومن الأفضل أن تبقيها دائمًا مفعّلة. في الحالات النادرة التي لا تعرف فيها فعلًا نوع المتغيًر، عليك التصريح على أنه من النوع any.

const a : any = /* افعل هذا إن كنت لا تعلم حقًا نوع المتغير */

لقد فعلنا القاعدة في مثالنا، لماذا إذًا لم يعترض المصرِّف على القيم التي تحمل النوع any ضمنًا؟ يعود السبب إلى الحقل query من كائن الطلب العائد للمكتبة express، فهو يمتلك النوع any صراحة. وكذلك الأمر بالنسبة للحقل request.body الذي يُستخدم لإرسال المعلومات إلى التطبيق.

هل يمكننا منع المطور من استخدام any نهائيًا؟ لدينا لحسن الحظ طرق أخرى غير استخدام tsconfig.ts لإجبار المطور على اتباع أسلوب محدد في كتابة الشيفرة. إذا يمكننا استخدام المدقق eslint لإدارة أسلوب كتابة الشيفرة. لنثبت eslint إذًا:

npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser

سنُهيئ المدقق بحيث يمنع التصريح بالنوع "any". اكتب القواعد التالية في الملف ذو اللاحقة "eslintrc.":

{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 11,
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint"],
  "rules": {
    "@typescript-eslint/no-explicit-any": 2
  }
}

لنكتب أيضًا السكربت lint داخل الملف package.json ليتحقق من وجود ملفات لاحقتها "ts.":

{
  // ...
  "scripts": {
      "start": "ts-node index.ts",
      "dev": "ts-node-dev index.ts",
      "lint": "eslint --ext .ts ."      //  ...
  },
  // ...
}

سيعترض المدقق لو حاولنا أن نصرّح عن متغير من النوع any.

eslint_disallow_any_15.png

ستجد في ‎@typescript-eslint الكثير من قواعد المدقق eslint المتعلقة باللغة TS، كما يمكنك استخدام القواعد الأساسية للمدقق في مشاريع هذه اللغة. من الأفضل حاليًا استخدام الإعدادات الموصى بها، ثم يمكننا لاحقًا تعديل القواعد عندما نحتاج إلى ذلك.

وعلينا الانتباه إلى التوصيات المتعلقة بأسلوب كتابة الشيفرة بما يتوافق مع الأسلوب الذي يتطلبه هذا القسم وكذلك وضع فاصلة منقوطة في نهاية كل سطر من أسطر الشيفرة.

سنستخدم حاليًا ملف قواعد المدقق الذي يتضمن ما يلي:

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "plugins": ["@typescript-eslint"],
  "env": {
    "node": true,
    "es6": true
  },
  "rules": {
    "@typescript-eslint/semi": ["error"],
    "@typescript-eslint/no-explicit-any": 2,
    "@typescript-eslint/explicit-function-return-type": 0,
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "no-case-declarations": 0
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  }
}

هناك بعض الفواصل المنقوطة المفقودة، لكن إضافتها أمر سهل.

والآن علينا إصلاح كل ما يحتاج إلى إصلاح.

التمرينان 9.6 - 9.7

9.6 المدقق ESlint

هيئ مشروعك ليستخدم الإعدادات السابقة وأصلح كل المشاكل.

9.7 تطبيق ويب لحساب ساعات التمرين

أضف وصلة تخديم إلى تطبيقك الذي يحسب ساعات التمرين اليومية. ينبغي أن تستخدم الوصلة لإرسال طلب HTTP-POST إلى وصلة التخديم المقابلة exercises متضمنة معلومات الدخل في جسم الطلب.

{
  "daily_exercises": [1, 0, 2, 0, 3, 0, 2.5],
  "target": 2.5
}

سيكون الجواب بصيغة json على النحو التالي:

{
    "periodLength": 7,
    "trainingDays": 4,
    "success": false,
    "rating": 1,
    "ratingDescription": "bad",
    "target": 2.5,
    "average": 1.2142857142857142
}

إن لم يكن جسم الطلب بالتنسيق الصحيح، أعد رمز الحالة المناسب مع رسالة الخطأ هذه:

{
  error: "parameters missing"
}

أو هذه:

{
  error: "malformatted parameters"
}

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

يمكن أن تجد أن التصريح عن متغير بأنه من النوع any مفيدًا في هذا التمرين عندما تتعامل مع البيانات الموجودة ضمن جسم الطلب. ستمنعك طبعًا قواعد تهيئة المدقق، لكن يمكنك أن تلغي تفعيل هذه القاعدة من أجل سطر محدد من الشيفرة بوضع هذا التعليق قبل السطر المطلوب.

// eslint-disable-next-line @typescript-eslint/no-explicit-any

انتبه بأنك ستحتاج إلى تهيئة صحيحة لتتمكن من الاحتفاظ ببيانات جسم الطلب. راجع القسم 3.

ترجمة -وبتصرف- للفصل First steps with TypeScript من سلسلة Deep Dive Into Modern Web Development


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...