{"id":"l-at-4-1","courseId":"advanced-typescript","lang":"ar","kind":"lesson","summary":"امنح القيم المتشابهة في الشكل أنواعًا مختلفة بالوسوم والمُنشئات الذكية، وأوقف خلط العملات وقت الترجمة، واجعل كل switch على union شاملًا بشكل مُثبَت باستخدام never.","html":"<p>خطأ الاسترداد من الدرس الأول كان سببه رقمين يبدوان متشابهين: الدولارات والسنتات. ولدى Cartwheel المشكلة نفسها في كل مكان. معرّف الطلب ومعرّف العميل كلاهما <code dir=\"ltr\">number</code>. والمبلغ بالسنتات والكمية كلاهما <code dir=\"ltr\">number</code>. والتنميط البنيوي، الذي تعرّفت عليه في <a href=\"/ar/lesson/advanced-typescript/l-at-1-2\">الـ Structural Typing وقابلية الإسناد</a>، يعتبر الأشياء المتشابهة في الشكل متطابقة، فيدعك بسرور تكتب <code dir=\"ltr\">cancelOrder(customer.id)</code>.</p>\n<p>الإصلاح أن تجعل الأشكال مختلفة، دون تغيير أي شيء وقت التشغيل.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الوسوم</h2>\n<p>الـ <strong>branded type (النوع الموسوم)</strong> هو تقاطع (intersection) بين نوع أولي وخاصية لا وجود لها إلا في نظام الأنواع:</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\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> brand</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> unique</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> symbol</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\"> Brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">B</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</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:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">readonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [brand]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> B</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\"> OrderId</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'OrderId'</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\"> CustomerId</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'CustomerId'</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\"> Cents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Cents'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span></code></pre></div>\n<p><code dir=\"ltr\">declare const brand: unique symbol</code> تعلن symbol لا يوجد أبدًا وقت التشغيل؛ واستخدامه مفتاحًا للخاصية يعني أنه لا يمكن لأي كائن حقيقي أن يملكه عن طريق الخطأ، ولا يستطيع أي شيء خارج هذا الملف تزويره بالاسم. يبقى <code dir=\"ltr\">OrderId</code> رقمًا لكل الأغراض (يمكنك مقارنته وطباعته وإرساله كـ JSON)، لكن TypeScript لم يعد يقبل رقمًا عاديًا أو <code dir=\"ltr\">CustomerId</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> cancelOrder</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:#953800;--shiki-dark:#FFA657\"> OrderId</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\">cancelOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(customer.id);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type 'CustomerId' is not assignable to parameter of type 'OrderId'.</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">cancelOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type 'number' is not assignable to parameter of type 'OrderId'.</span></span></code></pre></div>\n<h2 id=\"sec-2\" tabindex=\"-1\">المُنشئات الذكية</h2>\n<p>إن لم يكن الرقم العادي <code dir=\"ltr\">OrderId</code>، فمن أين تأتي قيم <code dir=\"ltr\">OrderId</code>؟ من دالة واحدة بالضبط لكل وسم، هي <strong>المُنشئ الذكي (smart constructor)</strong> الذي يتحقّق ثم يحوّل:</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> toOrderId</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</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:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderId</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isInteger</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(n) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Invalid order id: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">n</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:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderId</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> toCents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</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:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Cents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isSafeInteger</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(n)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Cents must be a whole number, got ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">n</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:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Cents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>هذا هو المكان المشروع الوحيد لـ <code dir=\"ltr\">as</code> في الكود الموسوم. ولأن التحويل لا يحدث إلا بعد التحقّق، فإن كل <code dir=\"ltr\">OrderId</code> في أي مكان من النظام معروف أنه معرّف صالح. الوسم إيصال: يثبت أن القيمة مرّت بالفحص.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 200\" role=\"img\" aria-labelledby=\"l-at-4-1-t1\"><defs><marker id=\"arrow-l-at-4-1-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-4-1-t1\">رقم عادي قادم من رابط أو JSON يمرّ عبر المُنشئ الذكي toOrderId، الذي يتحقّق منه فإمّا أن يرمي خطأً أو يُعيد OrderId. الدوال مثل cancelOrder لا تقبل إلا OrderId، فلا يستطيع الرقم العادي الوصول إليها مباشرة؛ ذلك المسار مسدود.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"70\" width=\"150\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"85\" y=\"96\" text-anchor=\"middle\">number</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"116\" text-anchor=\"middle\">من URL أو JSON</text>\n  <path class=\"d-arrow\" d=\"M160 98 L248 98\" marker-end=\"url(#arrow-l-at-4-1-1)\"/>\n  <rect class=\"d-box-warn\" x=\"252\" y=\"62\" width=\"180\" height=\"72\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"342\" y=\"92\" text-anchor=\"middle\">toOrderId(n)</text>\n  <text class=\"d-label-muted\" x=\"342\" y=\"116\" text-anchor=\"middle\">تحقّق، ثم حوّل</text>\n  <path class=\"d-arrow\" d=\"M432 98 L510 98\" marker-end=\"url(#arrow-l-at-4-1-1)\"/>\n  <rect class=\"d-box-success\" x=\"514\" y=\"70\" width=\"176\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"602\" y=\"96\" text-anchor=\"middle\">cancelOrder(id)</text>\n  <text class=\"d-label-muted\" x=\"602\" y=\"116\" text-anchor=\"middle\">تأخذ OrderId</text>\n  <path class=\"d-line d-dashed\" d=\"M85 126 L85 170 L602 170 L602 126\"/>\n  <text class=\"d-label-muted\" x=\"342\" y=\"162\" text-anchor=\"middle\">المسار المباشر: خطأ ترجمة</text>\n</svg><figcaption>المُنشئات الذكية هي البوابة الوحيدة إلى النوع الموسوم</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">مال يرفض الاختلاط</h2>\n<p>وسم السنتات نصف المهمة. والنصف الآخر هو العملة. اجعل العملة معامل نوع، فيصبح <code dir=\"ltr\">Money&lt;'USD'&gt;</code> و<code dir=\"ltr\">Money&lt;'EUR'&gt;</code> نوعين مختلفين:</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\"> Currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'EGP'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'AED'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'SAR'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'JOD'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'GBP'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'EUR'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'USD'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'CAD'</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\"> Money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Currency</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\">cents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Cents</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:#953800;--shiki-dark:#FFA657\"> C</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\"> add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Currency</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">a</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\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">b</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\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">NoInfer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>>)</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\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</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:#1F2328;--shiki-dark:#E6EDF3\"> { cents: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toCents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(a.cents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> b.cents), currency: a.currency };</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:#8250DF;--shiki-dark:#D2A8FF\">add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(usdPrice, usdShipping); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Money&#x3C;'USD'></span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(usdPrice, eurShipping);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type 'Money&#x3C;\"EUR\">' is not assignable to parameter of type 'Money&#x3C;\"USD\">'.</span></span></code></pre></div>\n<p>تفصيلان مهمّان. دون <code dir=\"ltr\">NoInfer</code>، سيكون كلا الوسيطين موقع استنتاج وسيصبح <code dir=\"ltr\">C</code> بهدوء <code dir=\"ltr\">'USD' | 'EUR'</code>، وهو بالضبط الفخ من <a href=\"/ar/lesson/advanced-typescript/l-at-2-3\">التحكّم في الـ Inference</a>. و<code dir=\"ltr\">a.cents + b.cents</code> هي <code dir=\"ltr\">number</code> عادية: الحساب ينزع الوسوم، لأن TypeScript لا يعرف إن كانت النتيجة ما زالت تعني “سنتات”. أعِد الوسم عن قصد، عبر المُنشئ، كي تُفحص قاعدة “السنتات أعداد صحيحة” من جديد.</p>\n<h3 id=\"sec-4\" tabindex=\"-1\">أين تؤتي الوسوم ثمارها</h3>\n<p>لا تسِم كل شيء. كل وسم يضيف استدعاء مُنشئ أينما دخلت القيم إلى النظام، وعلى بيانات الاختبار الجاهزة أن تمرّ به أيضًا. سِم حيث تتشارك قيمتان النوع الأولي نفسه، ويسهل الخلط بينهما، ويكون الخلط مكلفًا: معرّفات الكيانات المختلفة، والمبالغ بوحدات مختلفة، والنصوص التي يجب أن تجتاز تحقّقًا قبل الاستخدام، مثل <code dir=\"ltr\">Email</code> أو <code dir=\"ltr\">Sku</code>. أما <code dir=\"ltr\">quantity</code> التي لا تُضرب إلا في سعر فلا تحتاج وسمًا. في قاعدة كود كبيرة، نحو اثني عشر وسمًا مختارًا جيدًا يلتقط معظم أخطاء “النوع الصحيح، المعنى الخاطئ”؛ ومئة منها لا تولّد في الغالب إلا التحويلات.</p>\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\">JSON.parse</code> أو من معامل رابط أو من <code dir=\"ltr\">localStorage</code> رقم عادي، مهما قالت أنواعك عن الـ endpoint. إن حدّدت نوع استجابة API بـ <code dir=\"ltr\">{ id: OrderId }</code> وتخطّيت التحليل، فقد حوّلت بيانات غير متحقَّق منها إلى وسم، وهذا أسوأ من عدم وجود وسم إطلاقًا. سِم القيم أثناء تحليلها عند الحدود، وهو موضوع <a href=\"/ar/lesson/advanced-typescript/l-at-4-3\">تحليل البيانات غير الموثوقة عند الحدود</a>.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">الشمول باستخدام never</h2>\n<p>الوسوم تجعل القيم متمايزة. والنمط اليومي الآخر يجعل <em>المعالجة</em> كاملة. رأيت في <a href=\"/ar/lesson/advanced-typescript/l-at-1-4\">الـ Discriminated Unions لحالات المجال</a> أن جملة عودة ناقصة قد تكشف حالة منسيّة. فحص <code dir=\"ltr\">never</code> يجعل ذلك صريحًا، ويعمل حتى حين لا تُعيد الدالة شيئًا:</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> assertNever</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Unhandled value: ${</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">value</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> notifyCustomer</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 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\">  switch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (order.status) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'We are packing your order'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Track it: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">order</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">trackingNumber</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:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Enjoy!'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Cancelled: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">order</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cancelReason</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:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'returned'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Refund on its way'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    default</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> assertNever</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(order);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>في فرع <code dir=\"ltr\">default</code>، يكون كل عضو مُعالَج قد ضُيّق وأُزيل، فيكون <code dir=\"ltr\">order</code> من نوع <code dir=\"ltr\">never</code>، ويمرّ تمريره إلى معامل <code dir=\"ltr\">never</code> في الترجمة. أضف حالة <code dir=\"ltr\">'on-hold'</code> فيصبح <code dir=\"ltr\">order</code> في ذلك الفرع هو شكل on-hold، وهو غير قابل للإسناد إلى <code dir=\"ltr\">never</code>: خطأ ترجمة يشير إلى جملة الـ switch التي يجب تحديثها بالضبط. ووقت التشغيل، إن تسلّلت بيانات سيئة متجاوزةً الأنواع، ترمي الدالة خطأً برسالة واضحة بدل أن لا تفعل شيئًا.</p>\n<p>إن كنت تفضّل ألّا تعرّف دالة مساعدة، فإن <code dir=\"ltr\">default: throw new Error(`Unhandled: ${order satisfies never}`)</code> تُجري الفحص نفسه مباشرة. ولجداول البحث، يعطيك <code dir=\"ltr\">Record&lt;OrderStatus, string&gt;</code> مع <code dir=\"ltr\">satisfies</code> الشمول مجانًا: المفتاح الناقص خطأ ترجمة.</p>\n<p>في التمرين ستسِم معرّفات Cartwheel وسنتاته، وتجعل <code dir=\"ltr\">add</code> ترفض العملات المختلطة، وتضيف فحص شمول. والدرس القادم يتناول الشيء الآخر الذي يجب أن يتعامل معه كل نظام دفع: الإخفاقات المتوقَّعة.</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":"أين تؤتي الوسوم ثمارها","level":3},{"id":"sec-5","text":"الشمول باستخدام never","level":2}],"takeaways":["الوسم (brand) تقاطعٌ بين نوع أولي وخاصية فريدة لا وجود لها إلا في نظام الأنواع، فيتوقّف <code dir=\"ltr\">OrderId</code> و<code dir=\"ltr\">CustomerId</code> عن التبادل رغم أن كليهما رقم.","أنشئ القيم الموسومة فقط في مُنشئات ذكية تتحقّق أولًا؛ ذلك هو المكان الوحيد الذي يليق فيه تحويل <code dir=\"ltr\">as</code>.","الحساب على الأرقام الموسومة يُعيد <code dir=\"ltr\">number</code> عاديًا، لذا يجب أن تعيد دوال المال وسم نتائجها عن قصد.","<code dir=\"ltr\">Money&lt;NoInfer&lt;C&gt;&gt;</code> على الوسيط الثاني يمنع <code dir=\"ltr\">add(usd, eur)</code> من استنتاج union يخلط العملات.","فرع <code dir=\"ltr\">default</code> يمرّر القيمة إلى معامل من نوع <code dir=\"ltr\">never</code>، أو يفحصها بـ <code dir=\"ltr\">satisfies never</code>، يحوّل عضو الـ union المنسي إلى خطأ ترجمة."],"further":[{"title":"Unique symbol types","url":"https://www.typescriptlang.org/docs/handbook/symbols.html#unique-symbol"},{"title":"Exhaustiveness checking (Narrowing)","url":"https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking"}],"quiz":[{"q":"<p><code dir=\"ltr\">type OrderId = number &amp; { readonly [brand]: 'OrderId' }</code>. كيف تبدو خاصية الوسم وقت التشغيل؟</p>","options":[{"text":"كل قيمة <code dir=\"ltr\">OrderId</code> تحمل خاصية symbol مخفية.","why":"الأنواع الأولية لا تحمل خصائص، والوسم مُعلَن فقط ولا يُسند أبدًا. لا وجود له إلا في نظام الأنواع."},{"text":"تُغلَّف القيمة في كائن <code dir=\"ltr\">Number</code> مرفق به الـ symbol.","why":"لا شيء يغلّف القيمة. تحويل مثل <code dir=\"ltr\">n as OrderId</code> لا يولّد أي كود إطلاقًا."},{"text":"لا وجود لها؛ القيمة رقم عادي، والوسم يعيش في النوع فقط.","why":"صحيح. لهذا تكون الوسوم مجانية وقت التشغيل، ولهذا أيضًا لا تكون البيانات القادمة من <code dir=\"ltr\">JSON.parse</code> موسومة أبدًا حتى تُنشئها أنت."}],"answer":2},{"q":"<p>أين يجب أن تظهر <code dir=\"ltr\">as OrderId</code> في قاعدة كود جيدة التصميم؟</p>","options":[{"text":"حيثما احتاج رقم أن يُمرَّر إلى دالة تأخذ <code dir=\"ltr\">OrderId</code>.","why":"هذا يحوّل الوسم إلى زينة؛ يمكن تحويل أي رقم في أي مكان فتضيع الضمانة."},{"text":"فقط داخل المُنشئ الذكي الذي يتحقّق من الرقم أولًا.","why":"صحيح. نقطة دخول واحدة متحقَّق منها تعني أن كل <code dir=\"ltr\">OrderId</code> في النظام قد فُحص."},{"text":"لا مكان لها؛ يجب إنشاء الوسوم بـ <code dir=\"ltr\">satisfies</code>.","why":"<code dir=\"ltr\">satisfies</code> تفحص مقابل نوع دون تغييره، فلا تستطيع إضافة وسم لا تملكه القيمة."}],"answer":1},{"q":"<p>تُستدعى <code dir=\"ltr\">add&lt;C extends Currency&gt;(a: Money&lt;C&gt;, b: Money&lt;C&gt;)</code> هكذا: <code dir=\"ltr\">add(usd, eur)</code>. ماذا يحدث؟</p>","options":[{"text":"خطأ ترجمة، لأن العملتين مختلفتان.","why":"هذا ما تريده، لكن كلا الوسيطين موقع استنتاج، فيصبح <code dir=\"ltr\">C</code> هو <code dir=\"ltr\">'USD' | 'EUR'</code> ويمرّ الاستدعاء."},{"text":"يمرّ في الترجمة، لأن <code dir=\"ltr\">C</code> يُستنتج على أنه <code dir=\"ltr\">'USD' | 'EUR'</code>؛ علّم المعامل الثاني بـ <code dir=\"ltr\">Money&lt;NoInfer&lt;C&gt;&gt;</code> لرفضه.","why":"صحيح. مع <code dir=\"ltr\">NoInfer</code>، يأتي <code dir=\"ltr\">C</code> من الوسيط الأول وحده، ويفشل <code dir=\"ltr\">Money&lt;'EUR'&gt;</code> أمام <code dir=\"ltr\">Money&lt;'USD'&gt;</code>."},{"text":"يرمي خطأ وقت التشغيل لأن الوسم لا يطابق.","why":"الوسوم لا وجود لها وقت التشغيل، فلا شيء يمكن أن يرمي خطأً بسببها."}],"answer":1},{"q":"<p>أُضيفت حالة جديدة <code dir=\"ltr\">'on-hold'</code>. أيّ دالة تفشل في الترجمة إلى أن تتعامل معها؟</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">s</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</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\">  switch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (s) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Packing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">    // …one case for every other existing status…</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    default</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> assertNever</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(s);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"هذه الدالة، لأن <code dir=\"ltr\">s</code> في فرع <code dir=\"ltr\">default</code> تكون <code dir=\"ltr\">'on-hold'</code>، وهي غير قابلة للإسناد إلى <code dir=\"ltr\">never</code>.","why":"صحيح. فرع <code dir=\"ltr\">default</code> لا يرى إلا الأعضاء التي لم تعالجها أي حالة، و<code dir=\"ltr\">assertNever</code> لا تقبل أيًّا منها."},{"text":"لا شيء؛ <code dir=\"ltr\">default</code> تعالج كل قيمة، فيرضى الـ compiler.","why":"فرع <code dir=\"ltr\">default</code> العادي سيُخفي الحالة الجديدة. تمرير القيمة إلى معامل <code dir=\"ltr\">never</code> هو ما يجعله فحصًا."},{"text":"كل دالة تستخدم <code dir=\"ltr\">OrderStatus</code>، بما فيها تلك التي تخزّنها فقط.","why":"لا يتأثّر إلا الكود الذي يدّعي الشمول. الدوال التي تخزّن الحالة أو تمرّرها تبقى تمرّ في الترجمة."},{"text":"فقط الدوال التي تستخدم جمل <code dir=\"ltr\">if</code> بدل <code dir=\"ltr\">switch</code>","why":"الفحص يتعلّق بمعامل <code dir=\"ltr\">never</code>، لا بنوع الجملة المستخدمة؛ ويعمل مع سلاسل <code dir=\"ltr\">if</code> أيضًا."}],"answer":0}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"// Brand these three so they stop being interchangeable numbers.\ntype OrderId = number;\ntype CustomerId = number;\ntype Cents = number;\n\nfunction toOrderId(n: number): OrderId {\n  if (!Number.isInteger(n) || n < 1000) throw new Error(`Invalid order id: ${n}`);\n  return n;\n}\n\nfunction toCustomerId(n: number): CustomerId {\n  if (!Number.isInteger(n) || n < 1) throw new Error(`Invalid customer id: ${n}`);\n  return n;\n}\n\nfunction toCents(n: number): Cents {\n  if (!Number.isSafeInteger(n)) throw new Error(`Cents must be a whole number, got ${n}`);\n  return n;\n}\n\ntype Currency = 'USD' | 'EUR' | 'EGP';\ntype Money<C extends Currency = Currency> = { cents: Cents; currency: C };\n\nfunction money<C extends Currency>(cents: number, currency: C): Money<C> {\n  return { cents: toCents(cents), currency };\n}\n\nfunction add<C extends Currency>(a: Money<C>, b: Money<C>): Money<C> {\n  return { cents: toCents(a.cents + b.cents), currency: a.currency };\n}\n\ntype PaymentMethod =\n  | { kind: 'card'; last4: string }\n  | { kind: 'wallet'; provider: 'apple' | 'google' }\n  | { kind: 'cash-on-delivery' };\n\nfunction describePayment(p: PaymentMethod): string {\n  switch (p.kind) {\n    case 'card': return `Card ending ${p.last4}`;\n    case 'wallet': return `${p.provider === 'apple' ? 'Apple' : 'Google'} wallet`;\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<NotEqual<OrderId, number>>;\ntype T2 = Expect<NotEqual<OrderId, CustomerId>>;\ntype T3 = Expect<Equal<OrderId extends number ? true : false, true>>;\n\nfunction typeTests(customer: CustomerId, usd: Money<'USD'>, eur: Money<'EUR'>) {\n  // @ts-expect-error: a customer id is not an order id\n  const fromCustomer: OrderId = customer;\n  // @ts-expect-error: plain numbers must go through toOrderId\n  const fromNumber: OrderId = 1042;\n  // @ts-expect-error: dollars and euros cannot be added\n  add(usd, eur);\n  const total = add(usd, usd);\n  type T4 = Expect<Equal<typeof total, Money<'USD'>>>;\n}\n\nconsole.log(add(money(18900, 'USD'), money(499, 'USD')), describePayment({ kind: 'cash-on-delivery' }));\n"},"solution":{"ts":"declare const brand: unique symbol;\ntype Brand<T, B extends string> = T & { readonly [brand]: B };\n\ntype OrderId = Brand<number, 'OrderId'>;\ntype CustomerId = Brand<number, 'CustomerId'>;\ntype Cents = Brand<number, 'Cents'>;\n\nfunction toOrderId(n: number): OrderId {\n  if (!Number.isInteger(n) || n < 1000) throw new Error(`Invalid order id: ${n}`);\n  return n as OrderId;\n}\n\nfunction toCustomerId(n: number): CustomerId {\n  if (!Number.isInteger(n) || n < 1) throw new Error(`Invalid customer id: ${n}`);\n  return n as CustomerId;\n}\n\nfunction toCents(n: number): Cents {\n  if (!Number.isSafeInteger(n)) throw new Error(`Cents must be a whole number, got ${n}`);\n  return n as Cents;\n}\n\ntype Currency = 'USD' | 'EUR' | 'EGP';\ntype Money<C extends Currency = Currency> = { cents: Cents; currency: C };\n\nfunction money<C extends Currency>(cents: number, currency: C): Money<C> {\n  return { cents: toCents(cents), currency };\n}\n\nfunction add<C extends Currency>(a: Money<C>, b: Money<NoInfer<C>>): Money<C> {\n  return { cents: toCents(a.cents + b.cents), currency: a.currency };\n}\n\ntype PaymentMethod =\n  | { kind: 'card'; last4: string }\n  | { kind: 'wallet'; provider: 'apple' | 'google' }\n  | { kind: 'cash-on-delivery' };\n\nfunction assertNever(value: never): never {\n  throw new Error(`Unhandled value: ${JSON.stringify(value)}`);\n}\n\nfunction describePayment(p: PaymentMethod): string {\n  switch (p.kind) {\n    case 'card': return `Card ending ${p.last4}`;\n    case 'wallet': return `${p.provider === 'apple' ? 'Apple' : 'Google'} wallet`;\n    case 'cash-on-delivery': return 'Cash on delivery';\n    default: return assertNever(p);\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<NotEqual<OrderId, number>>;\ntype T2 = Expect<NotEqual<OrderId, CustomerId>>;\ntype T3 = Expect<Equal<OrderId extends number ? true : false, true>>;\n\nfunction typeTests(customer: CustomerId, usd: Money<'USD'>, eur: Money<'EUR'>) {\n  // @ts-expect-error: a customer id is not an order id\n  const fromCustomer: OrderId = customer;\n  // @ts-expect-error: plain numbers must go through toOrderId\n  const fromNumber: OrderId = 1042;\n  // @ts-expect-error: dollars and euros cannot be added\n  add(usd, eur);\n  const total = add(usd, usd);\n  type T4 = Expect<Equal<typeof total, Money<'USD'>>>;\n}\n\nconsole.log(add(money(18900, 'USD'), money(499, 'USD')), describePayment({ kind: 'cash-on-delivery' }));\n"},"checks":[{"id":"types","typecheck":true},{"id":"validates","test":"let threw = false; try { toOrderId(999); } catch { threw = true; } return threw && toOrderId(1042) === 1042;"},{"id":"adds","test":"const t = add(money(18900, 'USD'), money(499, 'USD')); return t.cents === 19399 && t.currency === 'USD';"},{"id":"cash","test":"return describePayment({ kind: 'cash-on-delivery' }) === 'Cash on delivery';"},{"id":"throws-unknown","test":"try { describePayment({ kind: 'crypto' }); return false; } catch { return true; }"}],"title":"سِم المعرّفات، واقفل العملة","prompt":"<p>قوِّ أنواع الدفع في Cartwheel:</p>\n<ol>\n<li>سِم <code dir=\"ltr\">OrderId</code> و<code dir=\"ltr\">CustomerId</code> و<code dir=\"ltr\">Cents</code> بحيث تتمايز عن <code dir=\"ltr\">number</code> وعن بعضها، واجعل المُنشئات الذكية الثلاثة تُعيد قيمًا موسومة.</li>\n<li>اجعل <code dir=\"ltr\">add</code> ترفض قيم <code dir=\"ltr\">Money</code> بعملات مختلفة.</li>\n<li>اجعل <code dir=\"ltr\">describePayment</code> تتعامل مع <code dir=\"ltr\">cash-on-delivery</code> (تُعيد <code dir=\"ltr\">'Cash on delivery'</code>) وأضف فحص شمول يفشل في الترجمة إن تُرك نوع دفع دون معالجة يومًا ما، ويرمي خطأً وقت التشغيل مع نوع مجهول.</li>\n</ol>\n","hints":["الوسم تقاطعٌ مع خاصية لا وجود لها إلا في النوع: <code dir=\"ltr\">declare const brand: unique symbol; type Brand&lt;T, B extends string&gt; = T &amp; { readonly [brand]: B };</code>.","المُنشئات الذكية هي المكان الوحيد للتحويل: <code dir=\"ltr\">return n as OrderId;</code>. وفي <code dir=\"ltr\">add</code>، تذكّر لماذا يتحوّل <code dir=\"ltr\">C</code> إلى union: كلا الوسيطين موقع استنتاج.","اكتب <code dir=\"ltr\">function assertNever(value: never): never { throw … }</code> واستدعها في فرع <code dir=\"ltr\">default:</code> بالقيمة المضيَّقة."],"explanation":"<p>التقاطع مع خاصية <code dir=\"ltr\">unique symbol</code> يجعل كل نوع موسوم مجموعة جزئية صارمة من <code dir=\"ltr\">number</code> لا يستطيع أي رقم عادي (أو وسم آخر) تحقيقها، دون أي كلفة وقت التشغيل. والتحويل داخل المُنشئات المتحقِّقة وحدها يعني أن كل قيمة موسومة قد فُحصت. و<code dir=\"ltr\">Money&lt;NoInfer&lt;C&gt;&gt;</code> تجعل الوسيط الثاني للفحص فقط، فيأتي <code dir=\"ltr\">C</code> من الوسيط الأول ويُرفض <code dir=\"ltr\">Money&lt;'EUR'&gt;</code>. وفي <code dir=\"ltr\">describePayment</code>، يتلقّى فرع <code dir=\"ltr\">default</code> كل ما لم تعالجه الحالات؛ وحين لا يتبقّى شيء يكون <code dir=\"ltr\">never</code> فتمرّ <code dir=\"ltr\">assertNever</code> في الترجمة، وإن تسلّل نوع ما وقت التشغيل فإنها ترمي خطأً بدل إعادة <code dir=\"ltr\">undefined</code>.</p>\n","checkLabels":{"types":"الملف يجتاز فحص الأنواع بلا أي خطأ، بما فيها كل اختبارات الأنواع","validates":"<code dir=\"ltr\">toOrderId</code> ترفض 999 وتقبل 1042","adds":"<code dir=\"ltr\">add</code> تجمع مبلغين بالعملة نفسها","cash":"<code dir=\"ltr\">describePayment</code> تتعامل مع الدفع عند الاستلام","throws-unknown":"<code dir=\"ltr\">describePayment</code> ترمي خطأً مع نوع دفع مجهول"}},"words":635,"readMinutes":4}