TYPE ⊆ SET منهج TypeScriptمن الجذور tsc --noEmit ✓
الإقليم ١01

النوع مجموعة Types are Sets

العدسة الكبرى التي يُقرأ بها كل شيء


النبذة

هذا أهم درس في المنهج كلّه. إن خرجت منه بعدسة واحدة فقط، خرجت بكل شيء. كل قاعدة في TypeScript — assignability، الاتحاد، التقاطع، never، unknown، التضييق، generics، حتى الرسائل الغريبة للأخطاء — هي نتيجة لجملة واحدة. لن نعطيك قواعد تحفظها. سنعطيك الجملة، ثم نُريك كيف تتساقط القواعد منها وحدها.

الجملة: النوع مجموعة (set) من القيم.


اللغز المستفزّ

إليك أربعة أسطر. لكل سطر، قرّر قبل أن تجرّب: هل يقبله المترجم أم يرفضه؟ والأهم — لماذا؟ لا تخمّن؛ ابنِ سبباً.

typescript
let a: number = 5; let b: 5 = 5; let c: number = b; let d: 5 = a;

السطر الثاني غريب: 5 كنوع؟ نعم، 5 نوع صالح في TypeScript. هذا وحده يجب أن يزعزع فكرتك عن "ما هو النوع". number نوع، و 5 نوع. ما العلاقة بينهما؟ ولماذا — خمّن الآن — السطر c يُقبل بينما السطر d يُرفض، رغم أنهما يبدوان معكوسين لبعض؟

لو لم تملك إجابة مقنعة، فأنت تملك قواعد محفوظة لا فهماً. بنهاية هذا الدرس ستشتقّها في ثانية.


الدرس: لماذا "مجموعة" هي العدسة الصحيحة

من أين تأتي الفكرة

ما النوع، حقاً؟ في C تقول "int يعني ٤ بايت بترميز معيّن". لكن هذا تعريف عن التمثيل في الذاكرة، وقد عرفنا في الإقليم ٠ أن أنواع TypeScript تُمحى ولا تمثّل شيئاً في الذاكرة. فذلك التعريف لا ينفعنا هنا. نحتاج تعريفاً لا يذكر الذاكرة أبداً.

اسأل بدلاً منه: حين أقول x: number، ما الذي أعدُ به عن قيمة x؟ أعدُ أن x ستكون واحدة من قيم معيّنة: 0، 1، 3.14، -7، NaN... ولن تكون أبداً "hello" أو true. إذاً number ليس "٤ بايت"؛ number هو اسمٌ لمجموعة القيم المسموح بها. كل القيم الرقمية الممكنة داخل المجموعة؛ كل ما عداها خارجها.

مبدأ

التعريف المؤسِّس: النوع T هو مجموعة من القيم. قول x: T يعني: "أتعهّد أن القيمة في x عضوٌ في المجموعة T." لا أكثر ولا أقل.

هذه العدسة تشرح فوراً السطر let b: 5 = 5. النوع 5 هو المجموعة التي تحوي قيمة واحدة فقط: {5}. مجموعة مفردة (singleton). وهذا نوع مشروع تماماً، لأن "مجموعة فيها عنصر واحد" مجموعة. النوع number هو المجموعة الضخمة {... كل الأرقام ...}. والعلاقة بينهما الآن واضحة كالشمس: {5} ⊂ {كل الأرقام}. المجموعة المفردة جزء من المجموعة الكبيرة.

علاقة الإسناد = علاقة الاحتواء بين المجموعات

الآن نشتقّ أهمّ قاعدة في اللغة كلها، مجاناً، من التعريف.

متى يصحّ let target: T = source؟ الإسناد يضع قيمة source في صندوق وُعِدَ أن كل ما فيه عضوٌ في T. فيصحّ فقط إذا كانت كل قيمة ممكنة لـ source هي أيضاً عضوٌ في T. أي:

ملاحظة

source يُسنَد لـ target: T ⟺ مجموعة source ⊆ مجموعة T.

هذا كل ما تعنيه كلمة "assignable" في كل رسالة خطأ ستراها في حياتك مع TypeScript. "Type X is not assignable to type Y" تعني حرفياً: مجموعة X ليست جزءاً من مجموعة Y — يوجد عنصر في X خارج Y، أي قيمة قد تأتي من X ولا يقبلها Y.

طبّقها على اللغز:

typescript
let c: number = b; // b نوعه {5}. هل {5} ⊆ {كل الأرقام}؟ نعم. ✅ let d: 5 = a; // a نوعه {كل الأرقام}. هل {كل الأرقام} ⊆ {5}؟ لا! ❌

السطر d يُرفض لأن a قد تحمل 7 (نوعها يسمح بذلك)، و 7 ∉ {5}. المترجم لا ينظر للقيمة الحالية 5؛ ينظر لـ المجموعة التي يسمح بها نوع a، وهي أوسع من أن تتّسع في {5}. هذا ليس تعنّتاً — إنه استدلال مجموعات سليم. لو سمح به، لانهار الوعد: غداً يصير a = 9، و d لا يزال يدّعي أنه 5.

ملاحظة

لاحظ الاتجاه. الإسناد يسير من الأضيق إلى الأوسع (subset → superset). تستطيع دائماً وضع شيء محدّد في صندوق عام، لا العكس. هذه الـ "subset → superset" هي سهم التوجيه (variance) الذي سيحكم الإقليم ٦ كلّه. خزّنها.

"النوع الفرعي" (subtype) هو فقط "المجموعة الفرعية"

في Java/C# تتعلّم "subtype" عبر شجرة وراثة: Dog نوع فرعي من Animal لأنك كتبت class Dog extends Animal. تعريف اسمي (nominal) — يعتمد على الاسم والإعلان الصريح.

في عدستنا، subtype لا علاقة له بالأسماء أو الوراثة:

ملاحظة

A نوع فرعي من B ⟺ المجموعة A ⊆ المجموعة B.

5 نوع فرعي من number لأن {5} ⊆ number. لم نُعلن وراثة؛ العلاقة قائمة لأن المجموعات هكذا. هذا أول إطلالة على أن TypeScript بنيوية (structural) لا اسمية — موضوع كامل في الإقليم ٥، لكن جذره هنا: العلاقات بين الأنواع تُحسب من محتوى المجموعات، لا من أسمائها أو إعلاناتها. (هذا قريب جداً من duck typing في Python — "إن كان يمشي كبطّة" — لكن مُبرهَناً وقت الترجمة لا مُكتشَفاً وقت التشغيل. سنعمّق هذا لاحقاً.)

لماذا هذه العدسة بالذات، ولماذا تستحقّ هذا التبجيل

لأنها توحّد كل ما هو متفرّق:

تعلّم قاعدة لكل واحدة من هذه = حفظ. أمّا أن تراها كلها أوجهاً لجبر المجموعات = فهم. هذا الفرق هو الفرق بين الحل الأعمى والحل الذي تعرف فيه ما تفعل.

حذرٌ واحد: العدسة دقيقة، لا كاملة

التمثيل "النوع = مجموعة" صحيح بنسبة ٩٥٪، ويكفي لاشتقاق كل شيء تقريباً. الـ ٥٪ الباقية حيث يتسرّب الواقع: any ليست مجموعة (هي "أطفئ المُبرهِن")، والـ assignability ليست دائماً احتواءً نظيفاً بسبب ثقوب مقصودة (الإقليم ٦) وبسبب أن المُبرهِن خوارزمية محدودة لا إله رياضي. سنُسمّي كل تسرّب حين نصل إليه. لكن حتى ذلك الحين: حين تحتار، ارسم المجموعات.


اللغز / التمرين

كل ما يلي يُحَلّ برسم دوائر فِن (Venn) ذهنياً وتطبيق "⊆". لا تشغّل المترجم إلا بعد أن تكتب توقّعك وسببه. الهدف أن تسبق المُبرهِن، لا أن تقلّده.

(أ) لكلٍّ ممّا يلي: مقبول أم مرفوض؟ ولماذا بلغة المجموعات؟

typescript
let x: "hi" = "hi"; let y: string = x; let z: "hi" = y; let p: boolean = true; let q: true = p;

(ب) النوع boolean في TypeScript يساوي بالضبط true | false. باستخدام عدسة المجموعات فقط، اشرح لماذا هذا ليس تعريفاً اعتباطياً بل حتميّ: ما القيم في مجموعة boolean؟ وما {true} ∪ {false}؟

(ج) لغز عكسي. صمّم نوعاً T بحيث: let a: T = "yes" يُقبل، و let b: T = "no" يُقبل، و let c: T = "maybe" يُرفض، و let d: T = 42 يُرفض. صف المجموعة التي تريدها بالضبط أوّلاً (عَدِّد عناصرها)، ثم ابحث في تركيبك للإقليم ٣ كيف تُكتب "مجموعة من عنصرين محدّدين". إن لم تعرف الصياغة بعد، اكتب المجموعة المطلوبة بالرموز ودعها سؤالاً مفتوحاً لِما بعد الإقليم ٣ — هذا مقصود.

(د) — الأصعب، تأمّلي. قلنا x نوعه قد يكون أوسع من قيمته الحالية (a نوعه number رغم أن قيمته 5). لكن let b: 5 = 5 أعطى b النوع الضيّق 5. خمّن: ما القاعدة التي يقرّر بها المترجم حجم المجموعة التي يستنتجها لمتغيّر حين لا تكتب النوع صراحةً؟ جرّب let m = 5 ثم مرّر الفأرة عليه في المحرّر، وجرّب const n = 5. هل المجموعتان متساويتان؟ لماذا قد تعتمد الإجابة على كون المتغيّر قابلاً لإعادة الإسناد؟ (هذه هي widening، نظرية الإقليم ٣ — لكن حدْسك يجب أن يصل إليها الآن من مبدأ "المجموعة تعكس ما قد تصيره القيمة، لا ما هي عليه الآن".)

ملاحظة

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


الخلاصة — وصلٌ في الشجرة

ركّبنا العدسة التي يقرأ بها المنهج كله:

  • النوع = مجموعة من القيم. number مجموعة كبيرة، 5 مجموعة مفردة {5}.
  • الإسناد ممكن ⟺ مجموعة المصدر ⊆ مجموعة الهدف. كل رسالة "not assignable" تعني "وُجد عنصر في المصدر خارج الهدف".
  • subtype = subset، محسوبٌ من المحتوى لا من الأسماء — أوّل بذرة للبنيوية (structural typing).
  • الإسناد يسير من الأضيق للأوسع؛ هذا السهم سيحكم التباين لاحقاً.

ربطنا للوراء: هذا يفسّر "العقد على شكل القيمة" من الإقليم ٠ بدقة أكبر — العقد هو المجموعة المسموح بها. وزرعنا للأمام بذرتين كبيرتين: ما أكبر مجموعة ممكنة وما أصغرها (الإقليم ٢)، وكيف نبني مجموعات جديدة بالاتحاد والتقاطع (الإقليم ٣).

بذرة غموض

بذرة غموض: قلنا 5 نوع، و number نوع، وبينهما . إذاً الأنواع مرتّبة في هرم: مجموعات أصغر تحت أكبر. سؤال: هل لهذا الهرم قمّة — مجموعة تحوي كل شيء؟ وهل له قاع — مجموعة لا تحوي شيئاً؟ ولو وُجد القاع، فما نفع نوعٍ لا قيمة فيه أبداً؟ يبدو عبثاً... لكنه سيكون أحد أقوى أدواتك. تابع للإقليم ٢.

التالي: 02-the-lattice.md

منهج TypeScript — من الجذور · نظرية مجموعات + مُبرهِن نظريات النوع = مجموعة