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

الشبكة The Lattice

القمّة والقاع والمواطنون الغرباء


النبذة

في الإقليم ١ صار لدينا هرم من المجموعات مرتّب بـ . سؤال الغموض كان: هل له قمّة وقاع؟ الجواب نعم، ولهما اسمان — unknown و never — وهما من أعمق وأجمل أدوات اللغة، رغم أن أحدهما يبدو للوهلة الأولى بلا معنى. سنقابل أيضاً ثلاثة مواطنين غرباء يكسرون التماثل: any (الخائن)، و void (المخادع)، و null/undefined (التوأم المنفيّ). فهم هؤلاء الخمسة يثبّت الشبكة كلها.


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

تأمّل هذه الأنواع الأربعة وقرّر — قبل أي تجربة — ما الذي يُقبل:

typescript
declare let u: unknown; declare let n: never; let a: string = u; // ؟ let b: unknown = "hi"; // ؟ let c: string = n; // ؟ let d: never = "hi"; // ؟

unknown و never معكوسان بطريقة مثاليّة: واحد يقبل الإسناد إليه من كل شيء لكن لا يُسنَد منه لشيء، والآخر بالعكس تماماً. أيّهما أيّ؟ ولو فهمت ذلك، فأجب عن اللغز الحقيقي:

ملاحظة

never هو النوع الذي لا قيمة تنتمي إليه أبداً. مجموعة خالية. متغيّر من نوع never لا يمكن أن يوجد. فما الفائدة الهندسية من نوعٍ مستحيل؟ اسم نوع لا قيمة له يبدو مزحة. لكنه ليس كذلك — إنه من أقوى ما في اللغة. فكّر: أين قد ينفعك بالضبط أن يقول النظام "هنا، رياضياً، لا يمكن أن تصل أي قيمة"؟


الدرس

القمّة: unknown = المجموعة الكلّية

الهرم في الإقليم ١ يرتفع كلّما اتّسعت المجموعة. القمّة هي المجموعة التي تحوي كل قيمة ممكنة: كل رقم، كل نص، كل كائن، null, undefined, الدوال، كل شيء. اسمها unknown (النوع الأعلى، top type).

اشتقّ سلوكها من العدسة، لا تحفظه:

ولهذا أيضاً، لا تستطيع فعل أي شيء بقيمة unknown مباشرة — لا u.length، ولا u()، ولا u + 1. عليك أولاً أن تضيّقها (الإقليم ٤) لتُثبت للمُبرهِن أنها في مجموعة أصغر تدعم العملية. unknown هي "صندوق مغلق آمن": تستطيع حمله وتمريره، لكن لا تفتحه إلا بعد إثبات محتواه.

القاع: never = المجموعة الخالية

انزل للقاع: المجموعة التي لا تحوي أي قيمة. اسمها never. وكل سلوكها — الذي يربك المبتدئين — يتساقط من تعريف المجموعة الخالية:

إذاً never و unknown انعكاسان مثاليّان للشبكة: unknown يبتلع كل شيء ولا يَخرج، و never لا يبتلع شيئاً ويُخرَج لكل مكان.

الآن، فائدة "النوع المستحيل": never ليس قيمةً تُنشئها، بل إشارة من المُبرهِن تقول: "وصلتُ، رياضياً، إلى نقطة لا قيمة تصلها." وهذا أنفع ممّا يبدو بمراحل:

  1. دالة لا تعود أبداً نوع إرجاعها never: دالة ترمي استثناءً دائماً، أو تدور للأبد. لا قيمة عائدة تنتمي لأي مجموعة، فالمجموعة خالية. `ts function fail(msg: string): never { throw new Error(msg); }`
  2. التحقّق من الشمول (exhaustiveness). هذا أجمل استعمالاته، وسنبنيه بالكامل في الإقليم ٤. الفكرة بذرتها هنا: لو ضيّقت مجموعةً حتى أفرغتها، يصير ما تبقّى never. فإذا أضفت لاحقاً حالة جديدة ونسيت معالجتها، لن تُفرَغ المجموعة، فلن يصل المتبقّي إلى never، فيشتكي المترجم. أي: never يتحوّل إلى منبّه آليّ عند إضافة حالة لم تُعالَج. نوعٌ "مستحيل" يصير حارساً.
نموذج ذهني

نموذج ذهني: unknown = "قد يكون أيّ شيء، أثبت قبل أن تلمس". never = "لا يمكن أن يكون أيّ شيء، أنت في فرعٍ ميّت". الأول أقصى الجهل، الثاني أقصى اليقين (السلبي).

المواطن الخائن: any

الآن المنطقة المظلمة. any يبدو كأنه القمّة (unknown) لكنه ليس مجموعة على الإطلاق. unknown تحوي كل القيم؛ any تُطفئ المُبرهِن. الفرق هائل:

typescript
let u: unknown = getValue(); u.toUpperCase(); // ❌ المترجم يرفض: لم تُثبت أنها نص let a: any = getValue(); a.toUpperCase(); // ✅ يمرّ بصمت — ثم ينفجر وقت التشغيل لو لم تكن نصاً

any يكسر الشبكة لأنه يُسنَد إلى أي شيء وأي شيء يُسنَد إليه في آنٍ واحد — وهو ما يستحيل لأي مجموعة حقيقية (فقط never تُسنَد للكل، وفقط unknown يَقبل من الكل؛ لا مجموعة تجمع الصفتين). any ليس عضواً في نظام المجموعات؛ إنه ثقب فيه: نقطة تقول "هنا، توقّف عن البرهان وثِق بي". وكل قيمة any تتسرّب عبر برنامجك حاملةً معها تعطيل الفحص، تُفسد السلامة في كل ما تلمسه.

اشتقاق عملي مباشر: القاعدة هي أن unknown بديلك الصحيح في كل مكان يغريك فيه any. كلاهما يقول "لا أعرف النوع"، لكن unknown يبقيك مُجبَراً على الإثبات قبل الاستعمال، بينما any يتخلّى عنك. حين ترى any في كود، اقرأها: "هنا أُسكِت المُبرهِن" — ثم اسأل لماذا، فغالباً يخفي خطأً.

(لماذا تُوجد any أصلاً إذاً؟ لأن TypeScript وُلدت فوق ملايين أسطر JavaScript موجودة، واحتاجت بوّابة هروب تدريجي لإدخالها. any ثمن التبنّي التدريجي، لا ميزة تصميمية. الإقليم ٩ يفصّل هذا الجانب من اللاسلامة.)

المخادع: void

void نوعٌ تراه في إرجاع الدوال التي "لا تُرجع شيئاً مفيداً":

typescript
function log(msg: string): void { console.log(msg); }

الفخّ: void ليست undefined، رغم أن الدالة فعلاً تُرجع undefined وقت التشغيل. void تعني شيئاً أدقّ: "لا تعتمد على القيمة العائدة؛ تجاهلها." وهذا يخلق سلوكاً يبدو متناقضاً حتى تفهم قصده:

typescript
type Fn = () => void; const f: Fn = () => 42; // ✅ يُقبل! رغم أن 42 ليست void

لماذا يُقبل إرجاع 42 حيث طُلب void؟ لأن void كنوع إرجاع متوقَّع يقول "لن أنظر للقيمة العائدة أصلاً"، فلا يضرّك أن الدالة أعادت شيئاً — سيُتجاهَل. هذا ليس خرقاً للعدسة بل تطبيق لها: المُستهلِك وعدَ بألّا ينظر، فأي قيمة آمنة. هذه الميزة هي ما يجعل arr.forEach(x => arr.push(x)) يعمل: push تُرجع رقماً، و forEach تتوقّع void، ولا أحد يكترث للرقم. (سنعود لهذا الاستثناء الدقيق ضمن التباين في الإقليم ٦؛ هنا يكفي أن void = "تُجاهَل القيمة" لا "القيمة undefined".)

التوأم المنفيّ: null و undefined، وقرار strict

في JavaScript قيمتان تعنيان "لا شيء": null و undefined. السؤال التصميمي: هل null عضوٌ في مجموعة string؟ أي هل let s: string = null صحيح؟

TypeScript تعطيك إجابتين حسب strictNullChecks (المُفعَّل ضمن "strict": true):

ملاحظة

هذا هو "ليش" قرار تشغيل strict الذي ألزمناك به في README. بدونه، الشبكة التي بنيناها مثقوبة من الأساس: null يتسلّل لكل مجموعة ويُبطل وعودها. مع strict، المجموعات تعني ما تقول. لا تطفئه أبداً.

اشتقاق جميل: لاحظ أن string | null يعني مجموعة string موسَّعة بعنصر null. وحين تفحص if (s !== null)، أنت تطرح {null} من المجموعة فتعود string نقيّة. هذا تضييق — الإقليم ٤ — لكنه الآن مفهوم كعملية مجموعات بحتة.

الشبكة مكتملة

unknown ← القمّة: كل القيم / | \ string number boolean {...} ← مجموعات واسعة | | | "hi" 5 true | false ← مجموعات مفردة \ | / never ← القاع: لا قيمة () any ─────────────────────────── خارج الشبكة: ثقبٌ يُعطّل البرهان

هذا ترتيب جزئي (partial order) مكتمل بقمّة وقاع — في الرياضيات يُسمّى lattice (شبكة)، ولهذا عنوان الإقليم. ولأنها شبكة، فلكل نوعين "أصغر حدٍّ أعلى مشترك" (الاتحاد) و"أكبر حدٍّ أدنى مشترك" (التقاطع) — وهذان بالضبط موضوع الإقليم التالي.


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

(أ) اشتقّ — بلغة المجموعات لا بالحفظ — نتيجة كلٍّ:

typescript
declare let u: unknown; declare let n: never; let a: unknown = n; // ؟ (∅ ⊆ unknown؟) let b: never = u; // ؟ let c: number = n; // ؟ function g(): never { while (true) {} } // ما نوع الإرجاع المنطقي؟ ولماذا never لا undefined؟

(ب) أنشئ متغيّراً نوعه unknown يحمل قيمة من API. اكتب كوداً يطبع طوله إن كان نصاً بشكل يقبله المُبرهِن. ستحتاج فحصاً يضيّق المجموعة. حاول قبل الإقليم ٤؛ سؤال موجّه: أي تعبير JavaScript يفصل "النصوص" عن بقية القيم؟ وكيف يربط المترجم هذا الفصل بتصغير المجموعة؟ (إن لم تصل، علّقها لِما بعد الإقليم ٤ — هذا مقصود.)

(ج) — لغز never الحقيقي. لديك:

typescript
type Shape = | { kind: "circle"; r: number } | { kind: "square"; s: number }; function area(shape: Shape): number { if (shape.kind === "circle") return Math.PI * shape.r ** 2; if (shape.kind === "square") return shape.s ** 2; // ما نوع shape هنا، بعد استبعاد الحالتين؟ }

المطلوب: أضِف سطراً أخيراً يستغلّ نوع shape في هذا الموضع بحيث: لو أضاف أحدٌ لاحقاً حالة ثالثة { kind: "triangle"; ... } ونسي معالجتها، يفشل الكود في الترجمة فوراً. فكّر: ما النوع الذي يصل لهذا الموضع الآن (بعد استبعاد كل الحالات)؟ وما النوع الذي سيصله لو بقيت حالة غير معالَجة؟ وكيف تجعل الفرق بينهما خطأ ترجمة؟ (كل ما تحتاجه: never يقبل فقط never. ابنِ من هذا. لا حلّ هنا.)

(د) — لماذا any خطر، تجريبياً. اكتب:

typescript
const data: any = JSON.parse('{"name": "Sam"}'); const len: number = data.name.length; // يمرّ const boom: number = data.age.length; // يمرّ أيضاً — لكن...

شغّله فعلياً بـ Node. أيّ سطر ينفجر، ومتى (ترجمة أم تشغيل)؟ الآن بدّل any بـ unknown وأعد المحاولة: ماذا يتغيّر، وفي أي لحظة يمسك المُبرهِن الخطأ؟ اكتب بجملة واحدة الفرق الجوهري بينهما من حيث "متى تُكتشف الكارثة".


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

أغلقنا الشبكة وفهمنا مواطنيها:

  • unknown = القمّة، المجموعة الكلّية: يقبل كل شيء، لا يُستعمل قبل تضييق.
  • never = القاع، المجموعة الخالية: لا قيمة فيه، يُسنَد لكل شيء، وأداتك لاكتشاف "الفروع المستحيلة" والشمول.
  • any = ليس مجموعة بل ثقب يُطفئ البرهان؛ بديله الآمن دائماً unknown.
  • void = "تُجاهَل القيمة العائدة"، لا undefined.
  • null/undefined = مجموعتان مفردتان منفصلتان مع strict — وهذا "ليش" إلزاميّة الوضع الصارم.

ربطنا للوراء: حوّلنا هرم الإقليم ١ إلى شبكة بقمّة وقاع. وربطنا للأمام: قلنا إن لكل نوعين اتحاداً وتقاطعاً — وأن طرح {null} بفحصٍ شرطي هو تضييق. هاتان العقدتان هما الإقليمان ٣ و ٤.

بذرة غموض

بذرة غموض: عرفنا القمّة والقاع. لكن أغلب الأنواع المفيدة في الوسط، ونحتاج أن نبنيها. كيف نصنع مجموعة "نص أو رقم"؟ أو "كائن له هذه وتلك الخاصية معاً"؟ ولماذا — استعدّ لمفاجأة — يكون string & number هو never؟ منطق المجموعات يتنبّأ بكل ذلك. للإقليم ٣.

التالي: 03-set-algebra.md

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