الأشكال Shapes
البنيوية وبطّة Python
النبذة
الكائنات قلب JavaScript، ونوع الكائن في TypeScript يخفي قراراً تصميمياً جذرياً يفرّقه عن Java و C#: البنيوية (structural typing). هنا نفهم لماذا اختارت TypeScript أن تحكم على الكائنات بشكلها لا باسمها — وهذا بالضبط duck typing الذي تعرفه من Python، لكن مُبرهَناً وقت الترجمة. ثم نواجه أغرب قاعدة في اللغة كلها: "فحص الخصائص الزائدة"، الذي يبدو نقضاً صريحاً للبنيوية حتى تكتشف لماذا وُجد.
اللغز المستفزّ
أربعة مقاطع. توقّع القبول/الرفض والسبب لكلٍّ:
typescripttype Point = { x: number; y: number }; // (1) const a = { x: 1, y: 2, z: 3 }; const p: Point = a; // ؟ // (2) const q: Point = { x: 1, y: 2, z: 3 }; // ؟ ← نفس القيمة، حرفياً! // (3) function dist(pt: Point) { return pt.x + pt.y; } dist({ x: 1, y: 2, color: "red" }); // ؟ // (4) class Cat { name = "felix"; } class Dog { name = "rex"; } const c: Cat = new Dog(); // ؟ ← في Java هذا كفرٌ مبين
ركّز على (1) مقابل (2): نفس القيمة بالضبط ({x:1, y:2, z:3}) تُسنَد لنفس النوع Point — مرّة عبر متغيّر، ومرّة مباشرة — فيُقبل أحدهما ويُرفض الآخر. هذا يبدو كخطأ في المترجم. ليس كذلك. لماذا تصنع الطريقة التي تكتب بها القيمة فرقاً في صلاحيتها؟
الدرس
ما الذي تعنيه مجموعة "نوع كائن"؟
ارجع للعدسة. النوع Point = { x: number; y: number } مجموعة من القيم. أي قيم؟ الفخّ أن تظنّها "الكائنات التي لها x و y فقط". خطأ. التعريف الصحيح:
{ x: number; y: number } = مجموعة كل القيم التي لها — على الأقل — خاصية x رقمية وخاصية y رقمية. ما زاد عن ذلك لا يُخرجها من المجموعة.
لماذا "على الأقل"؟ لأن النوع عقدٌ على ما تحتاجه لا على ما يوجد بالضبط. دالة dist تحتاج x و y؛ لو أعطيتها كائناً له x و y و z و color، فكلّ ما تحتاجه موجود، والزائد لا يضرّها — لن تلمسه. فمن منظور المجموعات، {x, y, z} عضوٌ في مجموعة {x, y}-type لأنه يستوفي كل شروطها.
هذا يفسّر (1) فوراً: a نوعه {x, y, z}، ومجموعته ⊆ مجموعة Point (كل كائن له الخصائص الثلاث له حتماً الخاصيّتين المطلوبتين). الإسناد سليم. وكذلك (3): الكائن الزائد عليه color يستوفي عقد Point.
لماذا البنيوية؟ ولماذا تختلف عن Java/C#
في Java، Cat c = new Dog() يُرفض حتى لو كان لـ Dog نفس حقول Cat تماماً — لأن Java اسمية (nominal): العلاقة بين نوعين تُقرَّر من أسمائهما وإعلانات الوراثة الصريحة، لا من شكلهما. Dog ليست Cat لأنها لم تُعلَن كذلك، نقطة.
TypeScript عكسها: بنيوية. العلاقة تُحسب من الشكل (البنية) فقط. لذلك (4) يُقبل: Dog له name: string، و Cat يطلب name: string، فمجموعة Dog ⊆ مجموعة Cat. الأسماء لا تعني شيئاً للمُبرهِن؛ المحتوى هو كل شيء.
ليش اختارت TypeScript البنيوية؟ لأنها وُلدت لتصف JavaScript الموجودة سلفاً. في JavaScript تُمرَّر كائنات حرفية {...} بلا أي إعلان صنف، وتعتمد الشيفرة على "إن كان له الخصائص المطلوبة، استعمله" — وهذا duck typing الذي تعرفه من Python: "إن مشى كبطّة ونَعَق كبطّة، فهو بطّة". لو فرضت TypeScript نظاماً اسمياً لأجبرت كل كائن على الانتماء لصنفٍ مُعلَن، وهذا يخالف روح اللغة ويكسر ملايين أسطر الكود القائم.
النموذج الذهني: TypeScript = duck typing الخاص بـ Python، لكن البطّة تُفحص وقت الترجمة بدل أن تُكتشف وقت التشغيل. في Python تكتشف أن الكائن لا يملك .quack() حين تستدعيها وتنفجر؛ في TypeScript يخبرك المُبرهِن قبل التشغيل. نفس الفلسفة، نقطة الفحص مختلفة. هذا الجسر بين ما تعرفه (Python) وما تتعلّمه يختصر عليك نصف الإقليم.
(وحين تحتاج فعلاً سلوكاً اسمياً — أن يكون نوعان مختلفين رغم تطابق شكلهما — تستطيع محاكاته. هذه حيلة "branded types"، نتركها للإقليم ٩ لأنها تكشف عن طبيعة النظام أكثر مما تكشف عن الكائنات.)
المفارقة: فحص الخصائص الزائدة (excess property checks)
الآن (2)، الذي يبدو ينقض كل ما سبق:
typescriptconst q: Point = { x: 1, y: 2, z: 3 }; // ❌ Object literal may only specify known properties
نفس قيمة (1) بالضبط، لكنها مرفوضة. لماذا؟ السبب ليس في المجموعات — {x,y,z} ما زالت ⊆ Point رياضياً. السبب حارسٌ خاص أضافته TypeScript فوق البنيوية، يعمل في حالة واحدة محدّدة: حين تُسنِد كائناً حرفياً (object literal) مباشرةً وجديداً لموضعٍ نوعُه معروف.
ليش هذا الحارس، إن كان يخالف البنيوية؟ لأن الخاصية الزائدة في كائنٍ حرفيّ مكتوب للتو تكاد دائماً تكون خطأً مطبعياً أو سوء فهم:
typescripttype Config = { color: string }; const c: Config = { colour: "red" }; // ❌ — وهذا بالضبط ما يُنقذك
لو طبّقنا البنيوية النقيّة، لكان { colour: "red" } كائناً له خاصية زائدة (colour) ويفتقر للمطلوبة (color)... لكنّ color اختيارية؟ لا، هي مطلوبة فهذا يُرفض. خذ مثالاً أوضح: { color: "red", widht: 5 } لنوعٍ خصائصه الإضافية اختيارية — البنيوية تقبله، لكنك غالباً أخطأت في widht. الحارس يفترض: "إن كتبت كائناً حرفيّاً للتو وفيه خاصية لا يعرفها النوع المستهدف، فأنت على الأرجح غلطت، لا أنك تريد بياناتٍ إضافية." فيشتكي — لكن فقط للحرفيّات الطازجة.
ولهذا (1) يمرّ: مرّرنا عبر متغيّر a. حين تمرّ القيمة عبر متغيّر، تكون قد "استقرّت" بنوعها الخاص، فالمترجم يطبّق البنيوية العادية ويتغاضى عن الزائد. الحارس يستهدف لحظةً واحدة: الحرفيّ المباشر الجديد، حيث الخطأ المطبعي أكثر احتمالاً.
هذا "ليش" يحلّ المفارقة كلها: فحص الخصائص الزائدة ليس جزءاً من نظام المجموعات؛ إنه طبقة إرشادٍ عملية (heuristic) فوقه، تضحّي بنقاء النظرية مقابل اصطياد أخطاء مطبعية شائعة جداً. حين تراه يشتكي، اقرأه: "هل هذه خاصية قصدتَها فعلاً، أم خطأ إملائي؟". ولو كنت تقصدها فعلاً، فالمخرج هو تمريرها عبر متغيّر، أو توسيع النوع، لا الكذب على المترجم.
خصائص أخرى تشتقّ من العدسة
- الاختيارية
?:{ x: number; y?: number }تعني أنyنوعها فعلياًnumber | undefined، وأن الكائن صالحٌ بوجودها أو غيابها. مجموعة أوسع من النسخة الإلزامية. اشتقّ: لماذا يجب أن تضيّقyقبل استعمالها رقماً؟ (لأنها قد تكونundefined— تضييق الإقليم ٤.) readonly: قيدٌ على الكتابة، وقت الترجمة فقط. اربطه بـconstفي C: كلاهما يمنع التعديل، لكنreadonlyفي TypeScript تُمحى تماماً — لا أثر لها وقت التشغيل، لا حماية حقيقية للذاكرة (عكسconstفي C الذي قد يضع البيانات في ذاكرة للقراءة فقط). إنها وعدٌ يفحصه المُبرهِن ثم يرميه. تذكّر دائماً جدار الإقليم ٠: حتى القيود تختفي.- index signatures
{ [k: string]: number }: "أي مفتاح نصّي يُربط بقيمة رقمية" — مجموعة الكائنات ذات المفاتيح المتجانسة (قاموس). اربطها بـdictفي Python.
الدوال أيضاً قيم — وعقودها أعمق
الكائنات لها شكل؛ والدوال كائنات في JavaScript، فلها أيضاً نوع: (x: number) => string. متى تكون دالةٌ ما عضواً في مجموعة هذا النوع؟ الجواب يبدو بديهياً — "تأخذ رقماً وتُرجع نصاً" — لكنه يخفي أعمق وأخدع سؤالٍ في كل اللغة: ماذا لو كانت دالتك تأخذ نوعاً أوسع أو تُرجع نوعاً أضيق؟ هل تبقى عضواً؟ هذا سؤال التباين (variance)، وهو ضخم لدرجة أنه يستحقّ إقليمه كاملاً.
اللغز / التمرين
(أ) اشتقّ كل نتيجة بلغة "على الأقل هذه الخصائص"، وميّز أين يتدخّل حارس الخصائص الزائدة:
typescripttype User = { id: number; name: string }; const u1: User = { id: 1, name: "a", age: 5 }; // ؟ const big = { id: 1, name: "a", age: 5 }; const u2: User = big; // ؟ لماذا يختلف عن u1؟ function greet(u: User) {} greet({ id: 1, name: "a", role: "admin" }); // ؟ const arr: User[] = [{ id: 1, name: "a", x: 9 }]; // ؟ هل الحارس يصل داخل المصفوفة؟
(ب) — البنيوية حيّة. عرّف type Quacker = { quack: () => void }. أنشئ كائناً duck وكائناً person (الأخير له quack و خصائص أخرى)، ومرّر كليهما لدالة makeNoise(q: Quacker). ثم أنشئ robot بلا quack وحاول تمريره. اربط بثلاث جمل: كيف يطابق هذا duck typing في Python، وأين يختلف زمنُ اكتشاف الخطأ؟
(ج) — لغز الخصائص الزائدة العكسي. لديك دالة تستقبل خيارات:
typescripttype Opts = { debug?: boolean }; function run(o: Opts) {} run({ debg: true }); // المترجم يشتكي
المطلوب ثلاث طرق مختلفة لجعل هذا يمرّ، كلٌّ منها يكشف زاوية مختلفة من القاعدة: (١) طريقة تُبقي السلامة وتكشف خطأك الحقيقي، (٢) طريقة عبر متغيّر وسيط، (٣) طريقة توسّع النوع. لكلٍّ، اكتب: هل حلّت الخطأ أم أخفته؟ أيّها الصحيح ولماذا؟ (الهدف أن ترى أن "إسكات المترجم" ≠ "إصلاح المشكلة" — تذكّر القاعدة الذهبية في README.)
(د) — readonly وهمٌ وقت التشغيل. اكتب:
typescripttype Frozen = { readonly x: number }; const f: Frozen = { x: 1 }; // f.x = 2; // المترجم يرفض const g = f as { x: number }; g.x = 99; // ؟ console.log(f.x); // ؟ ← f و g نفس الكائن
شغّله. هل تغيّر f.x فعلاً؟ اشرح بدلالة جدار الإقليم ٠ (الأنواع تُمحى) لماذا readonly لم تحمِ شيئاً وقت التشغيل، وقارنه بما يفعله Object.freeze() (الحماية الحقيقية الوحيدة). أي العالمين يسكن readonly؟
الخلاصة — وصلٌ في الشجرة
فهمنا الكائنات كمجموعاتٍ بنيوية:
- نوع الكائن = مجموعة القيم التي لها على الأقل الخصائص المطلوبة بأنواعٍ متوافقة؛ الزائد لا يُخرجها.
- TypeScript بنيوية (الشكل يحكم) لا اسمية (الاسم يحكم) — أي duck typing مُبرهَن وقت الترجمة، وُلد من ضرورة وصف JavaScript القائمة. جسرٌ مباشر لخبرتك بـ Python.
- فحص الخصائص الزائدة طبقةٌ إرشادية فوق البنيوية، تصطاد الأخطاء المطبعية في الكائنات الحرفية الطازجة — لا قاعدة مجموعات، بل مقايضة عملية.
?,readonly, index signatures كلها تشتقّ من العدسة؛ وreadonlyتذكّرنا أن حتى القيود تُمحى (جدار الإقليم ٠).
ربطنا للوراء: البنيوية هي "subtype = subset محسوبٌ من المحتوى" من الإقليم ١، الآن مكتملة. ربطنا للأمام: فتحنا سؤال متى تكون دالةٌ عضواً في نوع دالة — التباين.
بذرة غموض: قلنا إن Dog يُسنَد حيث يُطلب Cat لأن شكله أغنى. الآن اسأل عن المصفوفات: لو كان Dog يُسنَد حيث Animal، فهل Dog[] يُسنَد حيث Animal[]؟ يبدو بديهياً "نعم". لكن لو قبلتَ ذلك، أستطيع أن أدسّ قطّةً في مصفوفة كلابك وأكسر النظام بأكمله — وTypeScript تسمح لي، عن قصد. لماذا تقبل لغةٌ تُفاخر بالسلامة ثقباً كهذا؟ الجواب يكشف أعمق مقايضة في تصميمها. للإقليم ٦.
التالي: 06-functions-variance.md