JavaScript بعمق
TypeScript: مقدّمة عملية لمطوّري JavaScript
دليل عملي لـ TypeScript — لماذا تهمّ الأنواع الساكنة، والأنواع الأساسية والمستنتَجة، والواجهات وأسماء الأنواع، والاتحادات والأنواع الحرفية، والأنواع العامة، والاختياري وللقراءة فقط، والتضييق، وتنميط الدوال وعدم التزامن، وكيف يتكامل في مشروع حقيقي — مع تمارين عملية وحلولها.
TypeScript هي JavaScript مع نظام أنواع مركَّب عليها — وحالما تستخدمها، تبدو العودة كالبرمجة معصوب العينين. تلتقط فئة كاملة من الأخطاء قبل تشغيل الشيفرة (الأخطاء المطبعية، الوسائط الخاطئة، undefined نسيت معالجته)، وتشغّل الإكمال التلقائي وإعادة الهيكلة التي تجعل القواعد الكبيرة قابلة للإدارة. والأفضل: إنها تدريجية — JavaScript صحيحة هي TypeScript صحيحة، فتتبنّاها بقدر ما تشاء. هذه النواة العملية تغطّي معظم الاستخدام اليومي. (تفترض أساسيات JavaScript.)
TypeScript تضيف أنواعًا توجد فقط وقت الترجمة. يفحصها المترجم، ثم تُمحى — المتصفح يشغّل JavaScript عادية. فالأنواع لا تكلّف شيئًا وقت التشغيل؛ قيمتها كلها التقاط الأخطاء وإرشاد محرّرك أثناء الكتابة.
لماذا الأنواع
JavaScript العادية تدع هذا يُشحَن وينهار في الإنتاج:
function greet(user) { return "Hi " + user.name.toUpperCase(); }
greet({ nme: "Ada" }); // خطأ مطبعي — ينهار وقت التشغيل: name غير معرّف
TypeScript تلتقطه أثناء كتابتك:
function greet(user: { name: string }) { return "Hi " + user.name.toUpperCase(); }
greet({ nme: "Ada" }); // ❌ خطأ ترجمة: 'nme' لا يطابق { name: string }
يظهر الخطأ في محرّرك فورًا، والإصلاح واضح. اضرب ذلك عبر قاعدة كبيرة وتمنع الأنواع سيلًا ثابتًا من الأخطاء بينما توثّق النيّة.
الأنواع الأساسية والاستنتاج
تُعلّق بـ : type، لكن TypeScript تستنتج معظم الأنواع تلقائيًّا — نادرًا ما تعلّق المتغيّرات:
let name: string = "Ada";
let age: number = 36;
let active: boolean = true;
let tags: string[] = ["a", "b"]; // مصفوفة
let pair: [string, number] = ["x", 1]; // tuple — مواضع/أنواع ثابتة
let inferred = "hello"; // TS تستنتج `string` — بلا تعليق
علّق معاملات الدوال وأنواع الإرجاع (التي لا تُستنتَج من الاستخدام) ودع الاستنتاج يتولّى البقية:
function add(a: number, b: number): number {
return a + b;
}
نوعان خاصّان لمعرفتهما: any ينسحب من الفحص (تجنّبه — يُبطِل الهدف)، و**unknown** النسخة الآمنة التي تجبرك على الفحص قبل الاستخدام.
الواجهات وأسماء الأنواع
صِف شكل كائن بـ interface أو type — متبادلان إلى حدّ كبير للكائنات:
interface User {
id: number;
name: string;
email?: string; // اختياري — قد يغيب
readonly createdAt: Date; // لا يمكن إعادة إسناده بعد الإنشاء
}
type Point = { x: number; y: number }; // اسم نوع — الفكرة نفسها
function format(user: User): string {
return user.email ?? user.name;
}
? يعلّم خاصية اختيارية؛ وreadonly يمنع إعادة الإسناد. استخدم interface لأشكال الكائنات التي قد توسّعها، وtype للاتحادات والأسماء (أدناه) — لكن للكائنات العادية أيٌّ منهما جيّد.
الاتحادات والأنواع الحرفية
الاتحاد (|) يقول "واحد من هذه الأنواع"، والأنواع الحرفية تضيّق إلى قيم محدّدة — معًا تنمذجان المجالات الحقيقية بدقّة:
type Status = "loading" | "success" | "error"; // هذه السلاسل الثلاث فقط
type Id = string | number; // أيّ نوع
function setStatus(s: Status) { /* ... */ }
setStatus("success"); // ✅
setStatus("done"); // ❌ قيمة غير مسموحة
let value: string | null = getValue();
الاتحادات الحرفية من أنفع ميزات TypeScript — تحوّل "سلسلة يُفترَض أن تكون إحدى قيم قليلة" إلى شيء يفرضه المترجم.
التضييق
حين قد تكون قيمة عدّة أنواع، تضيّقها TypeScript بناءً على فحوصك — داخل if، تعرف النوع الأكثر تحديدًا:
function len(x: string | string[]): number {
if (typeof x === "string") {
return x.length; // هنا TS تعرف أن x سلسلة
}
return x.length; // هنا تعرف أنها string[]
}
function greet(name: string | null) {
if (name === null) return "Hi there";
return `Hi ${name.toUpperCase()}`; // null مُستبعَد سلفًا — آمن
}
هكذا تعالج TypeScript null/undefined بأمان: تجبرك على الفحص، ثم تعرف أن القيمة موجودة بعده. الفحص الصارم لـ null هو ما يقضي على "cannot read property of undefined".
الأنواع العامة
النوع العام (generic) معامل نوع — يتيح لدالّة أو نوع العمل مع أي نوع مع الحفاظ على العلاقة بين المُدخَل والمُخرَج:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // T = number، يُرجِع number | undefined
first(["a", "b"]); // T = string، يُرجِع string | undefined
// واجهة عامة
interface Box<T> { value: T; }
const b: Box<number> = { value: 42 };
بلا الأنواع العامة ستفقد معلومات النوع (إرجاع any) أو تكتب نسخة لكل نوع. T نائب يُملأ عند كل استدعاء، فتُبقي first نوع العنصر سليمًا. ترى الأنواع العامة في كل مكان: Array<T>، Promise<T>، Map<K, V>.
تنميط الدوال وعدم التزامن
// نوع دالّة
type Handler = (event: string) => void;
// غير متزامن — نوع الإرجاع وعد
async function loadUser(id: number): Promise<User> {
const res = await fetch(`/api/user/${id}`);
return res.json();
}
نوع إرجاع دالّة async دائمًا Promise<شيء>. تنميط الشكل المُحَلّ (Promise<User>) يعني أن كل ما يليه وينتظره يحصل على أمان أنواع كامل.
كيف يتلاءم في مشروع
TypeScript تترجم (tsc، أو عبر مُجمِّعك) ملفّات .ts إلى .js. tsconfig.json يضبطها؛ والإعداد المفتاحي "strict": true، الذي يفعّل الفحوص التي تجعل TypeScript تستحقّ الاستخدام (خاصّةً الفحص الصارم لـ null). عمليًّا يتولّى مُجمِّعك (Vite، إلخ) الترجمة، ومحرّرك يُظهر الأخطاء حيًّا، وتشحن JavaScript عادية. تبنَّ تدريجيًّا — أعد تسمية ملفّ إلى .ts، أضف أنواعًا حيث تساعد، ودع الاستنتاج يفعل البقية.
الأخطاء الشائعة
- اللجوء إلى
anyلإسكات خطأ — يعطّل الفحص ويخفي العلّة؛ فضّلunknownوضيّق. - الإفراط في التعليق حيث الاستنتاج كافٍ — علّق توقيعات الدوال، ودع المتغيّرات تُستنتَج.
- نسيان تفعيل
"strict"، ففوات أمان null الذي هو المكسب الأساسي. - الخلط بين أنواع وقت الترجمة ووقت التشغيل — الأنواع تُمحى؛ ما زلت تتحقّق من البيانات الخارجية (استجابات API) وقت التشغيل.
- كتابة
interfaces ضخمة حيثunionمن الحرفيات ينمذج المجال أفضل. - محاربة المترجم بتوكيدات النوع (
as) بدل إصلاح عدم تطابق النوع الفعليّ. - افتراض أن استجابة
.json()منمَّطة — إنهاany؛ نمِّطها صراحةً أو تحقّق منها.
تمارين
جرّب كلًّا منها قبل فتح الحل.
تمرين 1 — نمِّط دالّة
أضف أنواعًا إلى function double(n) { return n * 2; }.
إظهار الحل
function double(n: number): number {
return n * 2;
}
علّق المعامل ونوع الإرجاع؛ الآن double("x") خطأ ترجمة بدل إنتاج NaN وقت التشغيل.
تمرين 2 — نمذج حالة
عرّف نوعًا يسمح فقط بـ "idle" أو "active" أو "done"، ودالّة تقبله.
إظهار الحل
type State = "idle" | "active" | "done";
function transition(to: State) { /* ... */ }
transition("active"); // ✅
transition("paused"); // ❌ غير مسموح
اتحاد حرفيّ يحصر القيمة في تلك السلاسل الثلاث بالضبط، فتُلتقَط الأخطاء المطبعية والحالات غير الصالحة وقت الترجمة.
تمرين 3 — عالِج قيمة قابلة للإبطال
اكتب shout(name: string | null) يُرجِع "..." لـ null والاسم بأحرف كبيرة خلاف ذلك، بأمان أنواع.
إظهار الحل
function shout(name: string | null): string {
if (name === null) return "...";
return name.toUpperCase(); // TS تعرف أن name سلسلة هنا
}
فحص null يضيّق النوع، فبعده TypeScript واثقة أن name سلسلة و.toUpperCase() آمن.
تمرين 4 — هوية عامة
اكتب wrap<T> يضع أي قيمة في { value } مع الحفاظ على نوعها.
إظهار الحل
function wrap<T>(value: T): { value: T } {
return { value };
}
const a = wrap(42); // { value: number }
const b = wrap("hi"); // { value: string }
النوع العام T يلتقط نوع الوسيط فيبقى value الكائن المُرجَع منمَّطًا بدقّة، بدل الانهيار إلى any.
النموذج الذهني الذي تحتفظ به
TypeScript هي JavaScript زائد نظام أنواع وقت الترجمة يُمحى قبل التشغيل — أمان وأدوات صرفة، بلا كلفة وقت تشغيل. علّق توقيعات الدوال ودع الاستنتاج يتولّى المتغيّرات؛ صِف أشكال الكائنات بـ الواجهات/الأنواع، ونمذج المجالات بدقّة بـ الاتحادات الحرفية ("a" | "b")، ودع التضييق يجعل null/undefined آمنًا بفرض فحص. الجأ إلى الأنواع العامة (<T>) لإبقاء علاقات الأنواع سليمة عبر الشيفرة القابلة لإعادة الاستخدام، وتجنّب any (استخدم unknown وضيّق)، وفعّل strict للحصول على القيمة الحقيقية. إنها تدريجية، فتبنَّها ملفًّا تلو الآخر. وحالما تشعر بالمحرّر يلتقط علّة قبل أن تحفظ، تبدأ JavaScript العادية بالإحساس كالعمل بلا شبكة أمان.