{"id":"l-at-1-2","courseId":"advanced-typescript","lang":"ar","kind":"lesson","summary":"توقّع متى يقبل TypeScript نوعًا حيث يُنتظر نوع آخر، بما في ذلك فحوص الخصائص الزائدة ومعاملات الدوال، كي تكفّ أخطاء قابلية الإسناد عن مفاجأتك.","html":"<p>إليك دالة من عميل الدفع في Cartwheel، واستدعاءً يفاجئ القادمين من Java أو C#:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Money</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">amountCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> formatMoney</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">m</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">m</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">amountCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toFixed</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">m</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">currency</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { sku: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'TENT-2P'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Two-person tent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">18900</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, currency: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'USD'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(tent); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// compiles: \"189.00 USD\"</span></span></code></pre></div>\n<p>لم يُعرَّف <code dir=\"ltr\">tent</code> أبدًا على أنه <code dir=\"ltr\">Money</code>؛ إنه منتج. ومع ذلك يقبله TypeScript، لأنه لا يسأل عن اسم الشيء، بل يسأل إن كان له الشكل الصحيح. هذا هو <strong>الـ structural typing (التنميط البنيوي)</strong>، وحين تراه بوضوح تختفي معظم لحظات “لماذا يمرّ هذا الكود؟”.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الأنواع مجموعات من القيم</h2>\n<p>هذا أنفع نموذج ذهني في الدورة كلها: الـ type مجموعة من القيم. <code dir=\"ltr\">string</code> هو مجموعة كل النصوص. <code dir=\"ltr\">'USD'</code> مجموعة فيها قيمة واحدة. و<code dir=\"ltr\">'USD' | 'EUR'</code> فيها قيمتان.</p>\n<p><strong>قابلية الإسناد (assignability) هي علاقة الاحتواء.</strong> يمكن وضع قيمة من النوع A حيث يُنتظر B حين تكون كل A هي أيضًا B. كل <code dir=\"ltr\">'USD'</code> نصٌّ، لذا <code dir=\"ltr\">'USD'</code> قابل للإسناد إلى <code dir=\"ltr\">string</code>. وليس كل نص هو <code dir=\"ltr\">'USD'</code>، لذا يحتاج العكس إلى فحص.</p>\n<p>أنواع الكائنات تعمل بالطريقة نفسها، مع لمسة تُربك الناس. <code dir=\"ltr\">{ id: number }</code> هو مجموعة كل القيم التي لها <code dir=\"ltr\">id</code> رقمي. و<code dir=\"ltr\">{ id: number; status: string }</code> يضيف شرطًا ثانيًا، فتقلّ القيم المؤهَّلة. <strong>خصائص أكثر تعني مجموعة أصغر.</strong> لهذا يمكن تمرير <code dir=\"ltr\">Order</code> كامل حيث يُنتظر <code dir=\"ltr\">{ id: number }</code>: إنه في المجموعة الأصغر، فهو إذن في الأكبر أيضًا.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 280\" role=\"img\" aria-labelledby=\"l-at-1-2-t1\"><defs><marker id=\"arrow-l-at-1-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-at-1-2-t1\">مخطّطان لمجموعات متداخلة. على اليسار: unknown يحتوي string، الذي يحتوي الـ union من USD وEUR، الذي يحتوي USD. على اليمين: كائن فيه id يحتوي كائنًا فيه id وstatus، الذي يحتوي Order كاملًا. كل قيمة في مجموعة داخلية تنتمي أيضًا إلى المجموعات المحيطة بها.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"20\" width=\"320\" height=\"240\" rx=\"14\"/>\n  <text class=\"d-label-muted\" x=\"26\" y=\"46\">unknown</text>\n  <rect class=\"d-box-accent\" x=\"40\" y=\"60\" width=\"260\" height=\"180\" rx=\"12\"/>\n  <text class=\"d-label\" x=\"56\" y=\"86\">string</text>\n  <rect class=\"d-box-primary\" x=\"70\" y=\"100\" width=\"200\" height=\"120\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"86\" y=\"126\">'USD' | 'EUR'</text>\n  <rect class=\"d-box-success\" x=\"100\" y=\"146\" width=\"140\" height=\"56\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"170\" y=\"179\" text-anchor=\"middle\">'USD'</text>\n  <rect class=\"d-box\" x=\"350\" y=\"20\" width=\"320\" height=\"240\" rx=\"14\"/>\n  <text class=\"d-code\" x=\"366\" y=\"46\">{ id }</text>\n  <rect class=\"d-box-accent\" x=\"380\" y=\"60\" width=\"260\" height=\"180\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"396\" y=\"86\">{ id, status }</text>\n  <rect class=\"d-box-success\" x=\"410\" y=\"100\" width=\"200\" height=\"120\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"510\" y=\"150\" text-anchor=\"middle\">Order</text>\n  <text class=\"d-label-muted\" x=\"510\" y=\"176\" text-anchor=\"middle\">id, status, total…</text>\n</svg><figcaption>قابلية الإسناد هي علاقة الاحتواء: الأنواع الأضيق تقع داخل الأوسع</figcaption></figure>\n<p>يقف نوعان خاصّان عند الأطراف. <code dir=\"ltr\">unknown</code> هو مجموعة كل القيم: كل شيء قابل للإسناد إليه، وعليك أن تفحص قبل استخدامه. و<code dir=\"ltr\">never</code> هو المجموعة الفارغة: قابل للإسناد إلى كل شيء (فالمجموعة الفارغة جزء من كل مجموعة)، ولا شيء قابل للإسناد إليه. ستعتمد على الاثنين لاحقًا في الدورة. أما <code dir=\"ltr\">any</code> فليس مجموعة أصلًا؛ إنه يُطفئ الفحص في الاتجاهين، ولهذا يستطيع <code dir=\"ltr\">any</code> واحد شارد أن يسمّم بهدوء كل ما يلمسه.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">فحوص الخصائص الزائدة: الاستثناء الوحيد</h2>\n<p>إن كانت الخصائص الإضافية مقبولة، فلماذا يفشل هذا؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, stauts: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Object literal may only specify known properties, but 'stauts'</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// does not exist in type 'Order'. Did you mean to write 'status'?</span></span></code></pre></div>\n<p>هذا <strong>فحص الخصائص الزائدة (excess property check)</strong>، وهو قاعدة تقديرية مقصودة فوق التنميط البنيوي. حين تكتب كائنًا حرفيًا مباشرة حيث يُنتظر نوع، يكون الكائن “طازجًا”: لا شيء آخر يمكن أن يراه، فالخاصية الإضافية لا يمكن أن تكون إلا خطأً إملائيًا. لذلك يعلّمها TypeScript.</p>\n<p>ولا ينطبق الفحص إلا على الكائنات الحرفية الطازجة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, stauts: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'oops'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// compiles: draft is not fresh</span></span></code></pre></div>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الاعتماد على فحوص الخصائص الزائدة لحمايتك</span></p>\n<p>فحوص الخصائص الزائدة كاشفٌ للأخطاء الإملائية، لا ضمانة. ما إن تمرّ القيمة عبر متغيّر أو قيمة مُعادة من دالة أو تأكيد <code dir=\"ltr\">as</code> حتى تعبر الخصائص الإضافية بلا عائق. إذا كان يجب ألا تتلقّى دالةٌ حقولًا معيّنة (رقم بطاقة يذهب إلى أداة تسجيل مثلًا)، فلا تعتمد على هذا الفحص؛ اختر الحقول التي تحتاجها صراحةً.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">الدوال: المعاملات تقلب الاتجاه</h2>\n<p>في الدوال، تتبع أنواع القيم المُعادة القاعدة المعتادة، لكن المعاملات تسير في الاتجاه الآخر. الـ handler الذي يقبل <em>أي</em> <code dir=\"ltr\">CheckoutEvent</code> يمكن أن يحلّ محلّ handler لا يحتاج إلا <code dir=\"ltr\">PaymentFailed</code>. والعكس غير آمن: handler لا يفهم إلا <code dir=\"ltr\">PaymentFailed</code> سينهار أمام حدث <code dir=\"ltr\">shipped</code>. تحت <code dir=\"ltr\">strict</code> (وتحديدًا <code dir=\"ltr\">strictFunctionTypes</code>)، يرفضه TypeScript:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentFailed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'payment-failed'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">reason</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderShipped</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'order-shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">carrier</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentFailed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderShipped</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> failuresOnly</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentFailed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.reason.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toUpperCase</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()) };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> l</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> failuresOnly;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Type '{ onEvent: (e: PaymentFailed) => void; }' is not assignable to type 'Listener'.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">//   Types of property 'onEvent' are incompatible.</span></span></code></pre></div>\n<p>المعاملات الأقل مقبولة أيضًا: <code dir=\"ltr\">(e) =&gt; {}</code> و<code dir=\"ltr\">() =&gt; {}</code> كلتاهما تناسبان <code dir=\"ltr\">(e: CheckoutEvent, at: Date) =&gt; void</code>، لأن الدالة مسموح لها بتجاهل الوسائط. ولهذا تعمل <code dir=\"ltr\">orders.forEach((o) =&gt; …)</code> رغم أن <code dir=\"ltr\">forEach</code> تمرّر ثلاثة وسائط.</p>\n<h3 id=\"sec-4\" tabindex=\"-1\">ثغرة صيغة الـ method</h3>\n<p>اكتب الآن النوع نفسه بصيغة الـ method:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  onEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// method syntax</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> l</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> failuresOnly; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// compiles!</span></span></code></pre></div>\n<p>معاملات الـ methods المعرَّفة بهذه الطريقة تُفحص <strong>فحصًا ثنائي التغاير (bivariant)</strong>، حتى تحت <code dir=\"ltr\">strict</code>. يُبقي TypeScript هذا عن قصد كي تبقى الأنماط اليومية تعمل، مثل معاملة <code dir=\"ltr\">Array&lt;Dog&gt;</code> على أنها <code dir=\"ltr\">Array&lt;Animal&gt;</code>. والثمن أن الـ listener غير الآمن أعلاه يُقبَل.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>استخدم صيغة الخاصية للـ callbacks</span></p>\n<p>حين يصف الـ type دالة callback أو handler، اكتبه خاصيةً من نوع دالة: <code dir=\"ltr\">onEvent: (e: CheckoutEvent) =&gt; void</code>. تحصل على فحص كامل للمعاملات مجانًا. واحتفظ بصيغة الـ method للأصناف والكائنات التي تكون فيها الـ method فعلًا method.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">حين تريد أن تكون للأسماء قيمة</h2>\n<p>أحيانًا يكون التنميط البنيوي كريمًا أكثر من اللازم. <code dir=\"ltr\">OrderId</code> و<code dir=\"ltr\">CustomerId</code> كلاهما <code dir=\"ltr\">number</code>، فلا شيء يمنعك من تبديلهما. الحل أن تجعل الشكلين مختلفين، وهذا ما تفعله الـ branded types في <a href=\"/ar/lesson/advanced-typescript/l-at-4-1\">الـ Branded Types والفحوص الشاملة</a>. أما الآن فتكفيك القاعدة العامة: إن كان لشيئين الشكل نفسه، يعتبرهما TypeScript الشيء نفسه.</p>\n<p>التمرين أدناه يسدّ ثغرة الـ listener في ناقل الأحداث في Cartwheel. وبعده سترى كيف يضيّق TypeScript نوعًا واسعًا داخل الدالة، النصف الآخر من العمل مع المجموعات.</p>\n","toc":[{"id":"sec-1","text":"الأنواع مجموعات من القيم","level":2},{"id":"sec-2","text":"فحوص الخصائص الزائدة: الاستثناء الوحيد","level":2},{"id":"sec-3","text":"الدوال: المعاملات تقلب الاتجاه","level":2},{"id":"sec-4","text":"ثغرة صيغة الـ method","level":3},{"id":"sec-5","text":"حين تريد أن تكون للأسماء قيمة","level":2}],"takeaways":["يقارن TypeScript الأشكال لا الأسماء؛ أي قيمة تملك الخصائص المطلوبة قابلة للإسناد، أيًّا كان اسم نوعها.","اقرأ الـ type على أنه مجموعة القيم التي يسمح بها؛ نوع الكائن الذي فيه خصائص مطلوبة أكثر يصف مجموعة أصغر.","فحوص الخصائص الزائدة لا تنطبق إلا على الكائنات الحرفية الطازجة، ولهذا يمرّ تمرير متغيّر فيه حقل إضافي.","تُفحص معاملات الدوال في الاتجاه المعاكس، لكن الـ methods المعرَّفة بصيغة الـ method تُفحص فحصًا ثنائي التغاير (bivariant)، لذا فضّل صيغة الخاصية للـ callbacks."],"further":[{"title":"Type Compatibility","url":"https://www.typescriptlang.org/docs/handbook/type-compatibility.html"},{"title":"strictFunctionTypes","url":"https://www.typescriptlang.org/tsconfig/strictFunctionTypes.html"}],"quiz":[{"q":"<p>هل يمرّ هذا الكود في الترجمة؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Money</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">amountCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> product</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { sku: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'TENT-2P'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">18900</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, currency: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'USD'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> price</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Money</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> product;</span></span></code></pre></div>\n","options":[{"text":"نعم، لأن <code dir=\"ltr\">product</code> يملك كل خاصية يتطلّبها <code dir=\"ltr\">Money</code>، والخصائص الإضافية مسموحة في القيمة غير الطازجة.","why":"صحيح. <code dir=\"ltr\">product</code> متغيّر وليس كائنًا حرفيًا طازجًا، لذا لا يُفحص الحقل الزائد <code dir=\"ltr\">sku</code>؛ والشكل مطابق."},{"text":"لا، لأن <code dir=\"ltr\">product</code> لم يُعرَّف أبدًا على أنه <code dir=\"ltr\">Money</code>.","why":"TypeScript بنيوي؛ لا يسأل أبدًا بماذا عُرّفت القيمة، بل يسأل فقط إن كان شكلها مناسبًا."},{"text":"لا، لأن <code dir=\"ltr\">sku</code> خاصية زائدة.","why":"فحوص الخصائص الزائدة لا تعمل إلا على الكائنات الحرفية المكتوبة مباشرة حيث يُنتظر النوع. عبر متغيّر، الحقول الإضافية مقبولة."}],"answer":0},{"q":"<p>إذا فكّرت في الأنواع على أنها مجموعات من القيم، فأيّ العبارات صحيحة؟</p>","options":[{"text":"<code dir=\"ltr\">{ id: number; status: string }</code> مجموعة أكبر من <code dir=\"ltr\">{ id: number }</code>.","why":"كل خاصية مطلوبة شرطٌ إضافي على القيمة أن تحقّقه، فإضافة خاصية تُصغّر المجموعة لا تكبّرها."},{"text":"<code dir=\"ltr\">never</code> يحتوي كل القيم، ولهذا هو قابل للإسناد إلى كل شيء.","why":"<code dir=\"ltr\">never</code> هو المجموعة الفارغة. وهو قابل للإسناد إلى كل شيء لأن المجموعة الفارغة جزء من كل مجموعة."},{"text":"<code dir=\"ltr\">'EUR'</code> مجموعة جزئية من <code dir=\"ltr\">string</code>، فالقيمة <code dir=\"ltr\">'EUR'</code> قابلة للإسناد إلى <code dir=\"ltr\">string</code> والعكس غير صحيح.","why":"صحيح. قابلية الإسناد هي علاقة الاحتواء؛ المجموعة الأوسع لا تتدفّق إلى الأضيق دون فحص."},{"text":"<code dir=\"ltr\">unknown</code> و<code dir=\"ltr\">any</code> كلاهما المجموعة الفارغة.","why":"<code dir=\"ltr\">unknown</code> هو مجموعة كل القيم. و<code dir=\"ltr\">any</code> ليس مجموعة أصلًا؛ إنه يُطفئ الفحص في الاتجاهين."}],"answer":2},{"q":"<p>لماذا يمرّ هذا الكود في الترجمة تحت <code dir=\"ltr\">strict</code>؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'paid'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">|</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">carrier</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> l</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">carrier</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {} };</span></span></code></pre></div>\n","options":[{"text":"لأن الدوال السهمية لا تُفحص أبدًا مقابل الـ interfaces.","why":"الدوال السهمية تُفحص كأي دالة أخرى. التساهل مصدره طريقة تعريف <code dir=\"ltr\">onEvent</code>."},{"text":"لأن <code dir=\"ltr\">onEvent</code> تستخدم صيغة الـ method، ومعاملات الـ methods تُفحص فحصًا ثنائي التغاير حتى مع <code dir=\"ltr\">strictFunctionTypes</code>.","why":"صحيح. كتابة <code dir=\"ltr\">onEvent: (e: CheckoutEvent) =&gt; void</code> بدلًا منها تجعل فحص المعامل صارمًا وترفض هذا الـ listener."},{"text":"لأن المعامل من نوع union يقبل أيًّا من أعضائه.","why":"على الـ listener أن يتعامل مع كل أعضاء الـ union. قبول عضو واحد فقط غير آمن، والفحص الصارم كان سيلتقطه."}],"answer":1},{"q":"<p>أيّ استدعاء يرفضه فحص الخصائص الزائدة؟</p>","options":[{"text":"<code dir=\"ltr\">ship(order)</code> حيث <code dir=\"ltr\">order</code> متغيّر فيه حقل إضافي <code dir=\"ltr\">notes</code>","why":"المتغيّرات ليست طازجة، فلا تُفحص خصائصها الإضافية."},{"text":"<code dir=\"ltr\">ship({ ...order, notes: 'gift' })</code> حيث لا يحتوي <code dir=\"ltr\">Order</code> حقلًا اسمه <code dir=\"ltr\">notes</code>","why":"صحيح. الكائن الحرفي الذي فيه spread يبقى كائنًا حرفيًا طازجًا، و<code dir=\"ltr\">notes</code> مكتوب فيه مباشرة، فيُعلَّم. (أما الحقول الإضافية القادمة عبر <code dir=\"ltr\">...order</code> نفسه فلا تُفحص.)"},{"text":"<code dir=\"ltr\">ship(order as Order)</code> حيث <code dir=\"ltr\">order</code> فيه حقل إضافي <code dir=\"ltr\">notes</code>","why":"تأكيد النوع يطلب من الـ compiler أن يثق بك، فيتخطّى فحص الخصائص الزائدة كليًا."}],"answer":1}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type PaymentFailed = { type: 'payment-failed'; orderId: number; reason: string };\ntype OrderShipped = { type: 'order-shipped'; orderId: number; carrier: string };\ntype CheckoutEvent = PaymentFailed | OrderShipped;\n\n// Cartwheel's checkout event bus.\ninterface CheckoutListener {\n  onEvent(event: CheckoutEvent): void;\n}\n\nconst listeners: CheckoutListener[] = [];\nconst seen: string[] = [];\n\nfunction subscribe(listener: CheckoutListener): void {\n  listeners.push(listener);\n}\n\nfunction emit(event: CheckoutEvent): void {\n  for (const l of listeners) l.onEvent(event);\n}\n\n// Handles every event: must keep compiling.\nsubscribe({ onEvent: (e) => { seen.push(`${e.type}:${e.orderId}`); } });\n\n// A listener that takes no arguments at all: must keep compiling too.\nsubscribe({ onEvent: () => { seen.push('ping'); } });\n\n// ---- Type tests (do not edit) ----\n// This listener only understands PaymentFailed; an OrderShipped event would\n// crash it (e.reason is undefined). Subscribing it must be a type error.\nconst failuresOnly = { onEvent: (e: PaymentFailed) => { seen.push(e.reason.toUpperCase()); } };\n// @ts-expect-error: a PaymentFailed-only listener cannot receive every CheckoutEvent\nconst unsafe: CheckoutListener = failuresOnly;\n\nemit({ type: 'order-shipped', orderId: 1042, carrier: 'DHL' });\nconsole.log(seen);\n"},"solution":{"ts":"type PaymentFailed = { type: 'payment-failed'; orderId: number; reason: string };\ntype OrderShipped = { type: 'order-shipped'; orderId: number; carrier: string };\ntype CheckoutEvent = PaymentFailed | OrderShipped;\n\n// Cartwheel's checkout event bus.\n// Property syntax: the parameter is checked strictly (contravariantly).\ninterface CheckoutListener {\n  onEvent: (event: CheckoutEvent) => void;\n}\n\nconst listeners: CheckoutListener[] = [];\nconst seen: string[] = [];\n\nfunction subscribe(listener: CheckoutListener): void {\n  listeners.push(listener);\n}\n\nfunction emit(event: CheckoutEvent): void {\n  for (const l of listeners) l.onEvent(event);\n}\n\n// Handles every event: must keep compiling.\nsubscribe({ onEvent: (e) => { seen.push(`${e.type}:${e.orderId}`); } });\n\n// A listener that takes no arguments at all: must keep compiling too.\nsubscribe({ onEvent: () => { seen.push('ping'); } });\n\n// ---- Type tests (do not edit) ----\n// This listener only understands PaymentFailed; an OrderShipped event would\n// crash it (e.reason is undefined). Subscribing it must be a type error.\nconst failuresOnly = { onEvent: (e: PaymentFailed) => { seen.push(e.reason.toUpperCase()); } };\n// @ts-expect-error: a PaymentFailed-only listener cannot receive every CheckoutEvent\nconst unsafe: CheckoutListener = failuresOnly;\n\nemit({ type: 'order-shipped', orderId: 1042, carrier: 'DHL' });\nconsole.log(seen);\n"},"checks":[{"id":"types","typecheck":true},{"id":"still-delivers","test":"return seen.includes('order-shipped:1042') && seen.includes('ping');"}],"title":"سُدّ ثغرة الـ listener","prompt":"<p>يعرّف <code dir=\"ltr\">CheckoutListener</code> الدالة <code dir=\"ltr\">onEvent</code> بصيغة الـ method، فيفحص TypeScript معاملها فحصًا ثنائي التغاير (bivariant) ويقبل بلا تردّد <code dir=\"ltr\">failuresOnly</code>، وهو listener سينهار أمام حدث <code dir=\"ltr\">OrderShipped</code>. غيّر طريقة تعريف <code dir=\"ltr\">onEvent</code> (لا نوع معاملها) بحيث يصبح إسناد <code dir=\"ltr\">failuresOnly</code> إلى <code dir=\"ltr\">CheckoutListener</code> خطأ نوع، مع بقاء استدعاءَي <code dir=\"ltr\">subscribe</code> الموجودين يمرّان في الترجمة ووصول حدث الشحن إليهما.</p>\n","hints":["الإصلاح سطر واحد داخل الـ interface.","صيغة الـ method هي <code dir=\"ltr\">name(arg: T): void</code>. وصيغة الخاصية هي <code dir=\"ltr\">name: (arg: T) =&gt; void</code>.","اكتب <code dir=\"ltr\">onEvent: (event: CheckoutEvent) =&gt; void;</code>."],"explanation":"<p>مع صيغة الـ method لا ينطبق <code dir=\"ltr\">strictFunctionTypes</code>، فيمرّ listener لا يقبل إلا <code dir=\"ltr\">PaymentFailed</code> على أنه listener يقبل كل <code dir=\"ltr\">CheckoutEvent</code>. وحين تُعرَّف كخاصية من نوع دالة، يُفحص المعامل بتغاير معاكس: على الـ listener أن يقبل على الأقل كل ما يمكن أن يرسله الناقل. أما <code dir=\"ltr\">(e) =&gt; …</code> فتأخذ نوع معاملها من السياق، و<code dir=\"ltr\">() =&gt; …</code> تتجاهل الوسيط، فتبقى كلتاهما تمرّان. وصار لسطر <code dir=\"ltr\">@ts-expect-error</code> خطأ حقيقي يتوقّعه.</p>\n","checkLabels":{"types":"الملف يجتاز فحص الأنواع بلا أي خطأ: الـ listener غير الآمن مرفوض","still-delivers":"كلا الـ listeners الآمنَين ما زال يستقبل حدث الشحن"}},"words":648,"readMinutes":4}