الحواف The Edges
حيث ينكسر النظام، وأين تختفي الأنواع
النبذة
الإقليم الأخير، وفيه تلتحم الشجرتان اللتان نمتا بالتوازي منذ الإقليم ٠: شجرة "الأنواع كمجموعات" التي بنيناها بعناية، وشجرة "القيم وقت التشغيل" التي همسنا بها في خلفية كل درس. نقطة التحامهما هي الجدار: الأنواع تُمحى، فكل ما برهنّاه عن عالمٍ لا نراه لحظة تنفيذه. هنا نواجه ثلاث حقائق ناضجة: (١) لماذا لا يمكن لأي نظامٍ أن يكون سليماً وكاملاً وعملياً معاً — قَدَرٌ رياضي لا عيب تصميمي، (٢) خريطة كل الثقوب التي تركتها TypeScript عمداً، (٣) كيف تعبر الجدار بأمان، وتستعيد ما تخلّت عنه اللغة. تخرج من هنا لا "عارفاً TypeScript" بل فاهماً حدودها — وهذا ما يفصل من يستعملها عمّن يتقنها.
اللغز المستفزّ
كل أسطر هذا الكود تمرّ من المترجم بلا شكوى. شغّلها بـ Node:
typescript// (1) بيانات من "الشبكة" const res: User = JSON.parse('{"id": "not-a-number", "naem": "typo"}'); console.log(res.id.toFixed(2)); // 💥 // (2) تأكيد نوعٍ كاذب const el = document.querySelector(".btn") as HTMLButtonElement; el.click(); // 💥 لو لم يكن العنصر زرّاً (أو null) // (3) وصولٌ خارج الحدود const arr: number[] = [1, 2, 3]; const x: number = arr[10]; // النوع number — لكن القيمة undefined console.log(x.toFixed(2)); // 💥
المترجم — الذي حماك من ألف خطأ في الأقاليم السابقة — يصمت تماماً هنا، ثم ينفجر كلٌّ منها وقت التشغيل. لماذا خانك النظام الذي وثقت به؟ الجواب ليس "خطأ في TypeScript". الجواب هو كل هذا الإقليم: كلٌّ من هذه الثلاثة يعبر الجدار بين عالم الأنواع وعالم القيم، حيث لا سلطان للمُبرهِن.
الدرس
الحقيقة الأولى: لماذا لا توجد سلامةٌ كاملة — قَدَرٌ لا اختيار
في الإقليم ٦ قلنا "TypeScript ليست سليمة عن قصد". الآن نُعمّق: لا يمكن لأي نظام أنواعٍ عمليٍّ أن يكون الثلاثة معاً — سليماً (يرفض كل برنامجٍ خاطئ)، وكاملاً (يقبل كل برنامجٍ صحيح)، وقابلاً للحسم (يبتّ في وقتٍ منتهٍ). هذه ليست رأياً؛ إنها نتيجةٌ من نظرية الحوسبة. اشتقّ الحدْس:
سؤال "هل ينفجر هذا البرنامج وقت التشغيل؟" يكافئ — في حالته العامة — سؤال "هل يتوقّف هذا البرنامج؟" (مشكلة التوقّف)، وهي غير قابلة للحسم (تورنغ، ١٩٣٦). فأي مدقّقٍ ينتهي دائماً في وقتٍ محدود لا يستطيع أن يكون دقيقاً تماماً: عليه أن يخمّن. وله طريقان فقط في الخطأ:
- متحفّظ (sound): عند الشكّ، يرفض. لا يقبل برنامجاً خاطئاً أبداً، لكنه يرفض برامج صحيحة كثيرة. (هذا اتجاه Rust، Haskell — أمانٌ مقابل صرامةٍ تُتعب.)
- متساهل (unsound): عند الشكّ، يقبل. لا يزعجك بالبرامج الصحيحة، لكنه يفوّت بعض الخاطئة. (هذا اتجاه TypeScript — عمليّةٌ مقابل ثقوب.)
هذا "ليش" التصميمي الأعمق في المنهج كله: ثقوب TypeScript ليست أخطاءً يجب إصلاحها، بل موضعها المختار على طيفٍ لا مهرب منه. اختارت التساهل لأنها وُلدت لتُغطّي JavaScript الموجودة وتُرحّب بمبرمجيها، لا لتطردهم بالرفض. حين يفوّت المترجم خطأً، لا تغضب منه؛ افهم أنه على الطرف المتساهل من طيفٍ فرضته الرياضيات، وأن المسؤولية عن عبور الجدار عليك أنت.
الحقيقة الثانية: خريطة الثقوب (اعرف عدوّك بالاسم)
كل ثقبٍ هنا نقطةٌ تخلّى فيها المترجم عن البرهان. اقرأ كلاً منها كـ "هنا أنت وحدك":
١) any — الثقب الأمّ. يُطفئ المُبرهِن ويتسرّب لكل ما يلمس (الإقليم ٢). بديلك دائماً unknown.
٢) تأكيد النوع as — كذبةٌ موجَّهة. x as T يقول "ثق بي، إنها T" دون أي فحص. المترجم يصدّق ويُكمل على هذا الأساس. لغز (2) في المقدّمة: as HTMLButtonElement لم يفحص شيئاً وقت التشغيل؛ مجرّد وعدٍ منك، فإن كذبتَ انفجر. أخطر من any لأنه يبدو دقيقاً ومقصوداً. (انتبه: as ليست تحويلاً (cast) كما في C — لا تغيّر القيمة ولا بتّاتها، بل تغيّر فقط ما يظنّه المترجم. صفر أثرٍ وقت التشغيل.)
٣) ! non-null assertion. x! يقول "أعدك أنها ليست null/undefined". وعدٌ آخر بلا فحص. إن كذب، Cannot read properties of null.
٤) تباين المصفوفات والمناهج (bivariance). الإقليم ٦ بكامله — قبول Dog[] حيث Animal[]، مقابل ergonomics.
٥) فهرسة خارج الحدود. لغز (3): arr[10] نوعه number لكن قيمته undefined. المترجم — افتراضياً — يفترض أن كل فهرسٍ صالح. العَلَم noUncheckedIndexedAccess يُغلق هذا الثقب (يجعل arr[i] نوعه number | undefined)، لكنه ليس ضمن strict لأنه يُثقّل الكود الموجود. شغّله إن أردت صرامةً أعلى — واعرف أنك تختار موضعك على الطيف بنفسك.
٦) حدود العالم الخارجي — أكبر ثقبٍ على الإطلاق. JSON.parse, fetch().json(), localStorage, process.env, مدخلات المستخدم — كلها تعبر الجدار من العالم الحقيقي إلى برنامجك، وكلها نوعها فعلياً any أو نوعٌ تدّعيه أنت بلا برهان. لغز (1): JSON.parse يُرجع any، فادّعاؤك : User كذبةٌ لم يفحصها أحد. هذا ليس ثقباً جانبياً؛ إنه حيث تدخل كل أخطاء الإنتاج الحقيقية.
النمط الموحّد لكل الثقوب: كلٌّ منها لحظةٌ تقول فيها للمترجم "ثق بي" بدل أن يبرهن. any, as, !, والحدود الخارجية — كلها نقاط ثقةٍ يدوية. القاعدة الذهبية من README تتجسّد هنا: حين تكتب as أو !، اسأل "ما الادّعاء الذي أوقّع عليه الآن دون دليل؟ وماذا لو كان كاذباً؟".
الحقيقة الثالثة: كيف تعبر الجدار بأمان
الثقوب ليست نهاية القصّة؛ هي دعوةٌ لمسؤولية. عبور الجدار يتمّ بمكانٍ واحد: حيث يلتقي عالم الأنواع بعالم القيم، حوّل الادّعاء إلى فحصٍ حقيقي وقت التشغيل.
التحقّق وقت التشغيل (runtime validation). عند كل حدٍّ خارجي، لا تدّعِ النوع — افحصه بكودٍ يعمل فعلاً. تستطيع كتابة type predicate يدوياً (الإقليم ٤):
typescriptfunction isUser(v: unknown): v is User { return typeof v === "object" && v !== null && typeof (v as any).id === "number" && typeof (v as any).name === "string"; } const data: unknown = JSON.parse(raw); // unknown لا User! if (!isUser(data)) throw new Error("invalid payload"); data.id.toFixed(2); // ✅ الآن مبرهَنٌ *ومفحوصٌ* وقت التشغيل
لاحظ الأناقة: المُسنِد v is User يربط العالمين — فحصٌ حقيقي وقت التشغيل (عالم القيم) يُنتج معرفةً وقت الترجمة (عالم الأنواع). هذا هو الجسر الصحيح الوحيد فوق الجدار. (في الممارسة تُستعمل مكتبات مثل Zod التي تولّد الفحص والنوع معاً من تعريفٍ واحد — لكن جوهرها هو بالضبط ما كتبتَه يدوياً: predicate يفحص ثم يضيّق. الآن تفهم لماذا توجد، لا تستعملها كصندوقٍ أسود.)
القاعدة المعمارية: عامِل كل ما يدخل برنامجك من الخارج كـ unknown، وافحصه عند الحدّ، ثم تمتّع بالسلامة الكاملة في الداخل. الجدار صلبٌ عند الأطراف، والداخل آمن.
استعادة ما تخلّت عنه اللغة: الأنواع الاسمية (branded types)
في الإقليم ٥ قلنا إن البنيوية تمنع الأنواع الاسمية: UserId و PostId كلاهما number، فيُخلطان بحرية، رغم أن خلطهما خطأٌ منطقي:
typescriptfunction getUser(id: number) {} const postId = 42; getUser(postId); // يمرّ — لكنه خطأ دلالي صامت
كيف تستعيد سلوكاً اسمياً في نظامٍ بنيوي؟ بحيلةٍ تكشف عمق فهمك: اصنع فرقاً بنيوياً وهمياً لا وجود له وقت التشغيل:
typescripttype Brand<T, B> = T & { readonly __brand: B }; type UserId = Brand<number, "UserId">; type PostId = Brand<number, "PostId">;
الآن UserId و PostId مجموعتان مختلفتان بنيوياً (لكلٍّ __brand مختلف)، فلا تُخلطان — استعدنا السلوك الاسمي عبر البنيوية نفسها. والجمال: __brand لا وجود له أبداً وقت التشغيل (القيمة تبقى رقماً عادياً)؛ إنه خيالٌ يعيش في عالم الأنواع فقط، يُمحى مع كل شيء. أنت تستعمل جدار الإقليم ٠ سلاحاً: نوعٌ شبحيّ يحرس وقت الترجمة ويتبخّر قبل التشغيل. (تُدخل القيمة للنوع المُعلَّم عبر دالة فحصٍ تُصدِر as UserId بعد التأكّد — نقطة ثقةٍ واحدة محكومة، بدل ثقوبٍ منثورة.)
هذه الحيلة هي خلاصة المنهج كله في مثالٍ واحد: فهمت البنيوية (٥)، والتقاطع (٣)، وreadonly المُمحاة (٥)، وجدار الإمحاء (٠) — فركّبتها لتصنع ميزةً لا توفّرها اللغة مباشرة. هذا هو الفرق بين الحل الأعمى والاشتقاق من المبادئ: لم تحفظ "branded types"؛ اخترعتها لأنك فهمت القطع.
اللغز / التمرين — اللغز الختامي
(أ) — حصّن حدّاً. خذ لغز (1) من المقدّمة. أعِد كتابته بحيث: JSON.parse نتيجته unknown، ودالة isUser تفحص كل حقلٍ فعلياً، ولا يصل أي كودٍ للحقل id إلا بعد الفحص. ثم مرّر له JSON صحيحاً وآخر فاسداً، وتأكّد أن الفاسد يُرفض وقت التشغيل برسالةٍ واضحة لا بانفجارٍ غامض لاحق. اكتب: في أي سطرٍ بالضبط عبرتَ الجدار، وكيف حوّلت الادّعاء إلى برهان؟
(ب) — اصطَد ثقوبك. اكتب ثلاثة أسطرٍ تمرّ من المترجم وتنفجر وقت التشغيل، كلٌّ عبر ثقبٍ مختلف من القائمة (as، !، فهرسة خارج الحدود). لكلٍّ، اكتب "نقطة الثقة" التي وقّعتها، ثم أعِد كتابته بحيث يُغلق الثقب (فحص، أو noUncheckedIndexedAccess، أو تضييق). الهدف أن تدرّب عينك على رؤية الثقب قبل أن ينفجر.
(ج) — branded types. نفّذ UserId/PostId كاملةً: النوعان، دالتا إنشاء toUserId(n: number): UserId (مع فحصٍ مناسب)، ودالة getUser(id: UserId). تأكّد أن تمرير رقمٍ خام أو PostId يُرفض وقت الترجمة، وأن القيمة تبقى رقماً عادياً وقت التشغيل (اطبعها وتحقّق). اكتب بجملتين: أي القطع من الأقاليم ٠/٣/٥ استعملت، ولماذا __brand لا يكلّف شيئاً وقت التشغيل؟
(د) — التأمّل الختامي، لا كود. أجب كتابةً، فهذه خلاصة رحلتك كلها:
- ارسم الشجرتين المتوازيتين (الأنواع / القيم) وحدّد بالضبط أين تلتقيان، وسمِّ ثلاثة جسورٍ تعبر بينهما (تلميح: predicate، validation، branding).
- "TypeScript تبرهن نظرياتٍ عن عالمٍ لا تراه لحظة تنفيذه." اشرح هذه الجملة الآن بكلماتك، مستعيناً بثلاثة أمثلةٍ من المنهج. كيف تغيّر فهمك لها بين الإقليم ٠ والآن؟
- لو صمّمت لغةً جديدة، أين كنت ستضع مؤشّرها على طيف "سليم ↔ عملي"، ولماذا؟ لا توجد إجابة صحيحة — المطلوب أن تملك رأياً مبنياً على فهمٍ للمقايضة، وهذا بالضبط ما يفصل المتعلّم الأعمى عن من يعرف ما يفعل.
الخلاصة — والتحام الشجرة كاملةً
أغلقنا الرحلة عند الجدار:
- لا سلامةٌ كاملة ممكنة: سليم + كامل + قابل للحسم لا تجتمع (مشكلة التوقّف). ثقوب TypeScript موضعٌ مختار على طيفٍ حتميّ، لا أخطاء — اختارت التساهل لتُرحّب بـ JavaScript الموجودة.
- خريطة الثقوب:
any,as,!, تباين المصفوفات، الفهرسة، والحدّ الخارجي — كلها نقاط ثقةٍ يدوية تقول "ثق بي" بدل "أبرهن". - عبور الجدار: حوّل الادّعاء إلى فحصٍ وقت التشغيل عند كل حدٍّ خارجي (type predicates / validation)؛ عامل الخارج كـ
unknown، وافحص عند الطرف. - استعادة الاسمية: branded types تركّب البنيوية + التقاطع + الإمحاء لاختراع ما لا توفّره اللغة — تتويجٌ للاشتقاق على الحفظ.
التحام الشجرتين: منذ الإقليم ٠ نمت شجرةٌ تقول "الأنواع تُمحى" بصمت، بينما بنينا شجرة "الأنواع كمجموعات" بعناية. الآن تلتقيان عند الجدار: كل قوّة برهانية بنيناها (مجموعات، تضييق، تباين، برمجة على الأنواع) تعيش في عالمٍ يختفي قبل التشغيل. إتقان TypeScript ليس إتقان نصفٍ دون الآخر؛ إنه إتقان الجدار بينهما: أن تبرهن بقوّةٍ في الداخل، وتفحص بصرامةٍ عند الحدود.
لا بذرة غموض بعد الآن — بل دعوة. أنهيت الخريطة، لكن الأقاليم لا تُتقَن بقراءتها مرّة. عُد للألغاز التي علِقت فيها؛ ستراها الآن بعينٍ أخرى. وحين تدخل درسك القادم، لن تحلّ حلاً أعمى: ستسأل عند كل سطر "أي مجموعة؟ أي اتجاه احتواء؟ هل عبرتُ الجدار؟" — وهذا، منذ البداية، كان كل الهدف.
افتح الآن المخرجات المرافقة في appendix/ لترى الشجرة وقد اكتملت.