دليل عملي لـ TypeScript: بناء تطبيق Pokedex باستخدام HTML، CSS، و TypeScript

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

تُعد TypeScript طبقة إضافية فوق JavaScript، وتتميز بقدرتها على توفير تحكم أكبر في الشيفرة من خلال استخدام التعليقات التوضيحية للأنواع (type annotations)، والواجهات (interfaces)، والفئات (classes)، والتحقق الثابت من الأنواع (static type checking) الذي يكشف الأخطاء في مرحلة الترجمة (compile-time) قبل تشغيل التطبيق.

سنبدأ رحلتنا بتغطية جميع الأساسيات اللازمة للانطلاق مع هذه اللغة الرائعة، ثم سنختتم ببناء تطبيق عملي من الصفر: تطبيق Pokedex تفاعلي باستخدام HTML و CSS و TypeScript. هيا بنا ننطلق!

ما هي TypeScript؟

TypeScript هي لغة برمجة كائنية التوجه (object-oriented programming language) طورتها وتدعمها شركة مايكروسوفت. إنها مجموعة شاملة (superset) من JavaScript، مما يعني أن أي شيفرة JavaScript صالحة ستعمل كما هو متوقع في TypeScript. تجمع TypeScript بين جميع وظائف JavaScript وميزات إضافية قوية.

تتطلب TypeScript الترجمة (compilation) إلى JavaScript عادية أثناء وقت التشغيل (runtime)، لذا فأنت بحاجة إلى مترجم (compiler) لتحويل شيفرة TypeScript إلى JavaScript.

تستخدم TypeScript نظام الأنواع الثابتة (static typing)، مما يعني أنه يمكنك تحديد نوع للمتغير أثناء تعريفه. هذا الأمر لا يمكن تحقيقه مباشرةً في JavaScript لأنها لغة ذات أنواع ديناميكية (dynamically typed language) – لا تعرف نوع بيانات المتغير حتى يتم تعيين قيمة له في وقت التشغيل.

يُعد التحقق الثابت من الأنواع ميزة رائعة في TypeScript لأنه يساعد في اكتشاف الأخطاء في وقت الترجمة إذا كان المتغير غير مستخدم أو أعيد تعيينه بنوع مختلف. ومع ذلك، فإن هذا الخطأ لا يمنع الشيفرة من التنفيذ (وستظل شيفرة JavaScript الناتجة يتم إنشاؤها).

الأنواع الثابتة اختيارية في TypeScript. إذا لم يتم تعريف نوع ولكن المتغير لديه قيمة، فإن TypeScript سيستنتج النوع تلقائيًا. وإذا لم يكن للمتغير قيمة، فسيتم تعيين النوع الافتراضي any له.

الآن، دعنا نبدأ باستخدام TypeScript في القسم التالي لنرى كيف تعمل عمليًا.

إعداد بيئة عمل TypeScript

كما ذكرت سابقًا، تحتاج TypeScript إلى الترجمة إلى JavaScript عادية. لذلك، نحتاج إلى أداة للقيام بهذه الترجمة. وللوصول إلى هذه الأداة، يجب عليك تثبيت TypeScript عن طريق تشغيل أحد الأوامر التالية في الطرفية (terminal):

yarn add -g typescript

أو إذا كنت تستخدم npm:

npm install -g typescript

لاحظ أننا هنا نستخدم العلامة -g لتثبيت TypeScript بشكل عام (globally) بحيث يمكن الوصول إليها من أي مكان في نظامك.

بتثبيت TypeScript، أصبح لدينا الآن وصول إلى المترجم (compiler)، ويمكننا ترجمة شيفرتنا إلى JavaScript. سنتعمق لاحقًا في كيفية عمله، ولكن في الوقت الحالي، دعنا نضيف ملف إعدادات (configuration file) إلى مشروعنا. ليس من الضروري إضافة ملف إعدادات، ولكن في كثير من الحالات، يكون مفيدًا لأنه يسمح لنا بتعريف مجموعات قواعد للمترجم.

تهيئة TypeScript باستخدام ملف tsconfig

ملف tsconfig.json هو ملف بصيغة JSON يساعد في تهيئة TypeScript. وجود ملف إعدادات أفضل لأنه يساعد في التحكم بسلوك المترجم.

لإنشاء ملف الإعدادات، تحتاج أولاً إلى إنشاء دليل جديد باسم Pokedex والانتقال إلى الجذر (root) الخاص بهذا المجلد. ثم، افتحه في الطرفية أو بيئة التطوير المتكاملة (IDE) وقم بتشغيل هذا الأمر لإنشاء ملف إعدادات TypeScript جديد:

tsc --init

بمجرد إنشاء الملف، يمكننا الآن استكشافه في بيئة التطوير المتكاملة.

فهم محتويات ملف tsconfig.json

ملف tsconfig.json:

{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "public/js",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src"
]
}

هذا الملف الإعدادي أكثر تفصيلاً مما تراه أعلاه – لقد قمت بإزالة التعليقات والقيم غير المستخدمة لتسهيل القراءة. ومع ذلك، يمكننا الآن تحليل هذه القيم وشرح كل واحدة وما تفعله:

  • target: يحدد إصدار ECMAScript المستهدف عند ترجمة شيفرة TypeScript. هنا، نستهدف es5 لدعم جميع المتصفحات. يمكنك تغييره إلى ES6، ES3 (وهو الافتراضي إذا لم يتم تحديد هدف)، ES2020، وما إلى ذلك.
  • module: يحدد نمط الوحدة (module) للشيفرة المترجمة. يمكن أن يكون النمط Common JS، ES2015، ES2020، وما إلى ذلك.
  • outDir: يحدد دليل الإخراج (output directory) للشيفرة المترجمة إلى JavaScript.
  • rootDir: يحدد الموقع الذي توجد فيه ملفات TypeScript التي تحتاج إلى الترجمة.
  • include: يساعد في تحديد الدليل الذي يحتاج إلى الترجمة. إذا لم تكن لديك هذه القيمة، فسيقوم المترجم بأخذ كل ملف .ts وترجمته إلى JavaScript حتى لو تم تعريف دليل إخراج.

مع هذا الإعداد، يمكننا الآن الغوص في أحد أهم أجزاء TypeScript: الأنواع (Types).

أنواع البيانات في TypeScript

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

تحتوي TypeScript على عدة أنواع مثل: number، string، boolean، enum، void، null، undefined، any، never، array، و tuple. لن نرى جميع الأنواع في هذا الدليل، ولكن ضع في اعتبارك أنها موجودة.

الآن، دعنا نرى بعض الأمثلة على الأنواع الأساسية.

الأنواع الأساسية في TypeScript

let foo: string = "test"
let bar: number = 1
let baz: string[] = ["This", "is", "a", "Test"]

كما ترى هنا، لدينا ثلاثة متغيرات بأنواع مختلفة. المتغير foo يتوقع سلسلة نصية (string)، و bar يتوقع رقمًا (number)، و baz يتوقع مصفوفة من السلاسل النصية (array of strings). إذا استقبلت أيًا منها قيمة بخلاف النوع المعلن، فسيقوم TypeScript بإطلاق خطأ. يمكنك أيضًا تعريف baz بهذه الطريقة: let baz: Array<string> = ["This", "is", "a", "Test"].

الآن، دعنا نحاول إعادة تعيين أحد هذه المتغيرات ونرى كيف تتصرف TypeScript.

let foo: string = "test"
foo = 1

Type '1' is not assignable to type 'string'

سيقوم TypeScript بإطلاق خطأ لأننا أعلنا بالفعل أن foo يتوقع سلسلة نصية كقيمة. ويتم اكتشاف هذا الخطأ في وقت الترجمة، مما يجعل TypeScript رائعًا ومفيدًا.

مع TypeScript، يمكن أن تكون الأنواع صريحة (explicit) كما هو موضح أعلاه، ولكن يمكن أن تكون ضمنية (implicit) أيضًا. من الأفضل تحديد نوع قيمة معينة بشكل صريح لأنه يساعد المترجم والمطور التالي الذي يرث الشيفرة. ولكن يمكنك أيضًا تعريف المتغيرات بتعليق توضيحي ضمني للنوع.

let foo = "test"
let bar = 1
let baz = ["This", "is", "a", "Test"]

سيحاول TypeScript هنا استنتاج أكبر قدر ممكن لتوفير أمان النوع (type safety) بأقل قدر من الشيفرة. سيأخذ القيمة ويحددها كنوع للمتغير. ولن يتغير شيء بخصوص الأخطاء. دعنا نحاول إعادة تعيين هذه المتغيرات لنرى ما سيحدث.

foo = 7
bar = "updated"
baz = [2, true, "a", 10]

سيقوم TypeScript باكتشاف الأخطاء كما كان من قبل، حتى لو تم تعريف أنواع المتغيرات ضمنيًا.

Type '7' is not assignable to type 'string'.
Type '"updated"' is not assignable to type 'number'.
Type 'true' is not assignable to type 'string'.

عند التعامل مع كائن يحتوي على عدة خصائص، قد يكون تعريف الأنواع أمرًا صعبًا ومزعجًا. ولكن لحسن الحظ، لدى TypeScript ما يساعدك في هذه الحالة. لذا، دعنا نتعمق في واجهات TypeScript والأسماء المستعارة للأنواع (Type aliases) في القسم التالي.

الواجهات (Interfaces) والأسماء المستعارة للأنواع (Type Aliases)

تساعدنا الواجهات (Interfaces) والأسماء المستعارة للأنواع (Type aliases) في تحديد شكل هياكل البيانات الشبيهة بالكائنات (object-like data structures). تبدو متشابهة من حيث بنيتها، ولكن ضع في اعتبارك أنها مختلفة. ومع ذلك، فإن الإجماع بين المطورين هو استخدام interface كلما أمكن ذلك لأنه موجود في مجموعة قواعد tslint الافتراضية.

الآن، دعنا ننشئ واجهة واسمًا مستعارًا للنوع في القسم التالي لنرى كيف يعملان عمليًا.

interface ITest {
id: number;
name?: string;
}

type TestType = {
id: number,
name?: string,
}

function myTest(args: ITest): string {
if (args.name) {
return `Hello ${args.name}`
}
return "Hello Word"
}

myTest({ id: 1 })

كما ترى، تشبه بنية الواجهة والاسم المستعار للنوع كائن JavaScript. يجب عليهما تحديد شكل البيانات المعطاة باستخدام TypeScript. لاحظ أنني هنا أستخدم حقلًا اختياريًا name عن طريق إضافة علامة استفهام (?). هذا يسمح لنا بجعل الخاصية name اختيارية. هذا يعني أنه إذا لم يتم تمرير قيمة للخاصية name، فستعيد undefined كقيمة لها.

بعد ذلك، نستخدم الواجهة ITest كنوع للوسيط (argument) الذي تستقبله الدالة myTest. وكما هو الحال مع المتغيرات، يمكن أيضًا تعريف الدوال لإرجاع نوع معين. وهنا، يجب أن تكون القيمة المرجعة سلسلة نصية (string) وإلا سيتم إطلاق خطأ بواسطة TypeScript.

حتى الآن، لقد غطينا جميع المعارف الأساسية اللازمة للبدء مع TypeScript. الآن، دعنا نستخدمها لبناء تطبيق Pokedex باستخدام HTML و CSS.

صورة متحركة لبوكيمون يتطور، ترمز إلى تطور الشيفرة مع TypeScript

بناء تطبيق Pokedex باستخدام TypeScript

المشروع الذي سنقوم ببنائه سيجلب بيانات عن البوكيمونات من واجهة برمجة تطبيقات Pokemon API ويعرض كل بوكيمون باستخدام TypeScript. لذا، دعنا نبدأ بإنشاء ثلاثة ملفات جديدة في الجذر (root) لمجلد Pokedex: index.html، style.css، و src/app.ts. وبالنسبة لإعدادات TypeScript، سنستخدم نفس ملف tsconfig.json الذي أنشأناه سابقًا.

الآن، دعنا ننتقل إلى جزء الترميز (markup) ونضيف بعض المحتوى إلى ملف HTML.

الترميز (Markup)

ملف index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="style.css"/>
<title>TypeScript Pokedex</title>
</head>
<body>
<main>
<h1>Typed Pokedex</h1>
<div id="app"></div>
</main>
<script src="public/js/app.js"></script>
</body>
</html>

كما ترى، لدينا ترميز بسيط نسبيًا. هناك شيئان مهمان يجب الاحتفاظ بهما: المعرف id="app" لوسم div الذي سيستخدم لإلحاق المحتوى باستخدام TypeScript، ووسم script الذي يشير إلى المجلد public، وبالتحديد إلى ملف JavaScript الذي ستنشئه TypeScript لنا أثناء وقت الترجمة.

بالإضافة إلى ذلك، ملف CSS طويل بعض الشيء، لذا لن نغطيه – لا أريد إضاعة وقتك وأرغب في التركيز على TypeScript. ومع ذلك، يمكننا الآن الغوص فيه والبدء في جلب البيانات من واجهة برمجة التطبيقات (API).

جلب وعرض البيانات باستخدام TypeScript

نبدأ جزء TypeScript باختيار المعرف id="app" وهو معرف وسم div.

ملف src/app.ts:

const container: HTMLElement | any = document.getElementById("app")
const pokemons: number = 100

interface IPokemon {
id: number;
name: string;
image: string;
type: string;
}

هنا، لدينا تعليق توضيحي للنوع (type annotation) لم يتم تغطيته بعد. هذا هو نوع الاتحاد (Union Type) الذي يسمح بوجود أنواع بديلة لمتغير معين. هذا يعني أنه إذا لم يكن container من النوع HTMLElement، فسيقوم TypeScript بالتحقق مرة أخرى إذا كانت القيمة مساوية للنوع بعد رمز الأنبوب (|) وهكذا لأنك يمكن أن يكون لديك أنواع متعددة.

بعد ذلك، لدينا واجهة IPokemon التي تحدد شكل كائن البوكيمون الذي سيستخدم لاحقًا في الدالة المسؤولة عن عرض المحتوى.

ملف src/app.ts:

const fetchData = (): void => {
for (let i = 1; i <= pokemons; i++) {
getPokemon(i)
}
}

const getPokemon = async (id: number): Promise<void> => {
const data: Response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
const pokemon: any = await data.json()
const pokemonType: string = pokemon.types
.map((poke: any) => poke.type.name)
.join(", ")

const transformedPokemon = {
id: pokemon.id,
name: pokemon.name,
image: `${pokemon.sprites.front_default}` ,
type: pokemonType,
}

showPokemon(transformedPokemon)
}

تسمح لنا الدالة fetchData بالتكرار عبر عدد البوكيمونات المراد استردادها، ولكل كائن يتم استدعاء getPokemon برقم البوكيمون. قد يستغرق جلب البيانات وقتًا، لذلك سنستخدم دالة غير متزامنة (asynchronous function) تعيد Promise من النوع void. هذا الأخير يعني أن الدالة لن تعيد قيمة. وبمجرد جلب البيانات، يمكننا الآن إنشاء كائن جديد transformedPokemon يعكس الواجهة IPokemon، ثم تمريره كوسيط إلى showPokemon().

ملف src/app.ts:

const showPokemon = (pokemon: IPokemon): void => {
let output: string = `
<div class="card">
<span class="card--id">#${pokemon.id}</span>
<img class="card--image" src=${pokemon.image} alt=${pokemon.name} />
<h1 class="card--name">${pokemon.name}</h1>
<span class="card--details">${pokemon.type}</span>
</div>
`
container.innerHTML += output
}

fetchData()

كما ترى، تستقبل الدالة showPokemon كمعامل كائن البوكيمون من النوع IPokemon وتعيد void أو لا تعيد قيمة على وجه الدقة. ستضيف المحتوى فقط إلى ملف HTML بمساعدة المعرف container (تذكر، إنه وسم div).

رائع! لقد أنجزنا الكثير الآن، ولكن لا يزال هناك شيء مفقود لأن ملف index.html لن يعرض شيئًا إذا حاولت تشغيله في المتصفح. هذا لأن TypeScript تحتاج إلى الترجمة إلى JavaScript عادية. لذا، دعنا نفعل ذلك في القسم التالي.

ترجمة TypeScript إلى JavaScript

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

tsc

سيقوم هذا الأمر بترجمة كل ملف بامتداد .ts إلى JavaScript. وبما أن لدينا ملف tsconfig، فسيتبع المترجم القواعد المحددة ويترجم فقط ملفات TS الموجودة في مجلد src ويضع شيفرة JS في دليل public.

يسمح المترجم أيضًا بترجمة ملف واحد فقط:

tsc myFile.ts

وإذا لم تحدد اسمًا بعد ملف TS (myFile.ts)، فسيأخذ ملف JS المترجم نفس اسم ملف TS.

إذا كنت لا ترغب في تنفيذ الأمر عند كل تغيير، فما عليك سوى إضافة العلامة -w للسماح للمترجم بمراقبة التغييرات وإعادة ترجمة الشيفرة عند الحاجة:

tsc -w

والآن إذا قمت بتشغيل ملف index.html، سترى تطبيق Pokedex الخاص بك معروضًا بنجاح في المتصفح.

لقطة شاشة لتطبيق Pokedex مكتمل يعرض قائمة من البوكيمونات

رائع! لقد تعلمنا الآن أساسيات TypeScript من خلال بناء تطبيق Pokedex باستخدام HTML و CSS. يمكنك معاينة المشروع المكتمل هنا أو العثور على الشيفرة المصدرية هنا. يمكنك أيضًا العثور على محتوى رائع آخر مثل هذا على مدونتي أو متابعتي على تويتر لتلقي الإشعارات عندما أكتب شيئًا جديدًا. شكرًا للقراءة.

مصادر إضافية

إليك بعض المصادر المفيدة للتعمق أكثر في TypeScript:

الخلاصة التقنية

لقد أثبت هذا الدليل العملي أن TypeScript ليست مجرد إضافة لـ JavaScript، بل هي أداة قوية تعزز بشكل كبير من موثوقية الشيفرة وسهولة صيانتها، خاصة في المشاريع الكبيرة. من خلال فرض الأنواع الثابتة (سواء بشكل صريح أو ضمني)، تمكن TypeScript المطورين من اكتشاف الأخطاء المحتملة في وقت الترجمة بدلاً من وقت التشغيل، مما يوفر وقتًا وجهدًا كبيرين في عملية التصحيح. كما أن استخدام الواجهات (interfaces) والأسماء المستعارة للأنواع (type aliases) يوفر هيكلية واضحة للبيانات، مما يحسن من قابلية قراءة الشيفرة ويسهل التعاون بين أعضاء الفريق. بناء تطبيق Pokedex كان مثالاً ممتازًا لكيفية دمج TypeScript بسلاسة مع تقنيات الويب الأساسية مثل HTML و CSS لإنشاء تطبيقات ويب قوية ومنظمة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *