{"id":"l-at-4-2","courseId":"advanced-typescript","lang":"ar","kind":"lesson","summary":"أعِد الإخفاقات المتوقَّعة كقيم محدَّدة الأنواع عبر union من نوع Result، كي يُضطر المستدعون إلى معالجة البطاقة المرفوضة أو الكوبون المنتهي، واحتفظ بالاستثناءات للأخطاء البرمجية وما هو غير متوقَّع حقًا.","html":"<p>إليك توقيع الدالة التي في قلب نظام الدفع في Cartwheel:</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> placeOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">cart</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Cart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">payment</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentMethod</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> { </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* … */</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span></code></pre></div>\n<p>يمكن أن تفشل بخمس طرق على الأقل يتوقّعها العمل كل يوم: البطاقة مرفوضة، أو منتج نفد من المخزون، أو الكوبون منتهي الصلاحية، أو العنوان لا يمكن التوصيل إليه، أو مزوّد الدفع يطلب 3-D Secure. التوقيع لا يذكر أيًّا منها. إنها استثناءات تُرمى من مكان ما في الداخل، وليس في TypeScript جملة <code dir=\"ltr\">throws</code>. المستدعي الذي ينسى <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code> يمرّ في الترجمة بلا مشكلة، والعميل يرى شاشة فارغة.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">جعل الإخفاق جزءًا من النوع</h2>\n<p>الإصلاح أن تُعيد الإخفاقات المتوقَّعة بدل رميها، باستخدام discriminated union تعرف كتابته أصلًا:</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\"> Result</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\">E</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\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</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:#953800;--shiki-dark:#FFA657\"> T</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\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">error</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</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:#8250DF;--shiki-dark:#D2A8FF\"> ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</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\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</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\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</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:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ ok: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, value });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> err</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">E</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">error</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</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\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">error</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ ok: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, error });</span></span></code></pre></div>\n<p><code dir=\"ltr\">ok</code> هو المميِّز. لا يستطيع المستدعي قراءة <code dir=\"ltr\">value</code> دون أن يثبت أولًا أن الاستدعاء نجح، لأن <code dir=\"ltr\">value</code> لا توجد إلا في عضو واحد:</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\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> placeOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cart, payment);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">showConfirmation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(result.value);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Property 'value' does not exist on type 'Result&#x3C;Order, CheckoutError>'.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">//   Property 'value' does not exist on type '{ ok: false; error: CheckoutError; }'.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (result.ok) </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">showConfirmation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(result.value); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// fine</span></span></code></pre></div>\n<h2 id=\"sec-2\" tabindex=\"-1\">أخطاء لها بنية</h2>\n<p>يمكن أن يكون <code dir=\"ltr\">E</code> نصًا، لكن النص في الأخطاء هو نظير <code dir=\"ltr\">status: string</code> في الحالات. نمذج الإخفاقات كـ discriminated union، كلٌّ منها بالبيانات اللازمة لمعالجته:</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\"> CheckoutError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span></span>\n<span class=\"line\"><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\"> 'card-declined'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">declineCode</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\">  |</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\"> 'out-of-stock'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">sku</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:#953800;--shiki-dark:#FFA657\">available</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>\n<span class=\"line\"><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\"> 'coupon-expired'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">code</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:#953800;--shiki-dark:#FFA657\">expiredOn</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\"> errorMessage</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\"> CheckoutError</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\"> (e.type) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'card-declined'</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\"> `Your card was declined (${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">declineCode</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\"> 'out-of-stock'</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\"> `Only ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">available</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} of ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">sku</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} left.`</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\"> 'coupon-expired'</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\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">code</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} expired on ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expiredOn</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>نوع الإرجاع <code dir=\"ltr\">: string</code> يجعل هذا شاملًا، كما رأيت في القسم 1. وحين يضيف فريق المدفوعات <code dir=\"ltr\">'requires-3ds'</code> إلى الـ union، تُعلَّم كل دالة رسائل وكل فرع في الواجهة يتفرّع على <code dir=\"ltr\">type</code>.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">تركيب الخطوات</h2>\n<p>الدفع تسلسل: تحقّق من السلة، ثم طبّق الكوبون، ثم اخصم من البطاقة. كل خطوة يمكن أن تفشل بأخطائها الخاصة. والعودة المبكرة تركّبها دون تداخل:</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CartError</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\"> 'out-of-stock'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">sku</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:#953800;--shiki-dark:#FFA657\">available</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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CouponError</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\"> 'coupon-expired'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">code</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:#953800;--shiki-dark:#FFA657\">expiredOn</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> checkout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutInput</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">CartError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CouponError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentError</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\"> cart</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateCart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input.lines);</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\">cart.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cart;</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\"> priced</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> applyCoupon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cart.value, input.coupon);</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\">priced.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> priced;</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\"> charge</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> chargeCard</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(priced.value.total, input.payment);</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\">charge.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> charge;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> ok</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">createOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(priced.value, charge.value));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>بعد <code dir=\"ltr\">if (!cart.ok)</code>، يضيق <code dir=\"ltr\">cart</code> إلى <code dir=\"ltr\">{ ok: false; error: CartError }</code>، وهو قابل للإسناد إلى نوع الإخفاق الأوسع للدالة، فيمكن إعادته كما هو. ومسار النجاح يُقرأ من الأعلى إلى الأسفل، ونوع الإرجاع يوثّق كل إخفاق على المستدعي معالجته. وإن أضاف أحدهم لاحقًا خطوة رابعة بنوع خطأ جديد ونسي توسيع نوع الإرجاع، فإن <code dir=\"ltr\">return</code> لإخفاق تلك الخطوة يكون خطأ ترجمة، فلا يمكن لقائمة الإخفاقات أن تتقادم بصمت.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 210\" role=\"img\" aria-labelledby=\"l-at-4-2-t1\"><defs><marker id=\"arrow-l-at-4-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-4-2-t1\">مخطّط على هيئة سكّة. ثلاث خطوات، validateCart وapplyCoupon وchargeCard، تقع على مسار نجاح يؤدّي إلى ok مع Order. ومن كل خطوة يهبط فرع إلى مسار إخفاق ينتهي بـ Result فيه ok يساوي false والـ union من CartError وCouponError وPaymentError.</title>\n  <rect class=\"d-box-accent\" x=\"10\" y=\"40\" width=\"140\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"80\" y=\"69\" text-anchor=\"middle\">validateCart</text>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"40\" width=\"140\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"270\" y=\"69\" text-anchor=\"middle\">applyCoupon</text>\n  <rect class=\"d-box-accent\" x=\"390\" y=\"40\" width=\"140\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"460\" y=\"69\" text-anchor=\"middle\">chargeCard</text>\n  <rect class=\"d-box-success\" x=\"580\" y=\"40\" width=\"110\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"635\" y=\"69\" text-anchor=\"middle\">ok(order)</text>\n  <path class=\"d-arrow\" d=\"M150 64 L196 64\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M340 64 L386 64\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M530 64 L576 64\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M80 88 L80 146\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M270 88 L270 146\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M460 88 L460 146\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <rect class=\"d-box-warn\" x=\"10\" y=\"150\" width=\"680\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"350\" y=\"179\" text-anchor=\"middle\">{ ok: false, error: CartError | CouponError | PaymentError }</text>\n</svg><figcaption>كل خطوة إمّا تستمر على مسار النجاح أو تخرج بخطئها</figcaption></figure>\n<p>سترى مكتبات تقدّم methods مثل <code dir=\"ltr\">map</code> و<code dir=\"ltr\">andThen</code> لسلسلة النتائج بدلًا من ذلك. لا بأس بها، لكن في TypeScript يكون أسلوب العودة المبكرة عادةً أسهل قراءة ولا يحتاج إلى مكتبة: الـ narrowing يقوم بالعمل.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">تغليف الكود الذي يرمي استثناءات</h2>\n<p>معظم الإخفاقات في نظام الدفع تبدأ حياتها كاستثناءات في كود شخص آخر: الـ SDK الخاص بالدفع يرمي <code dir=\"ltr\">CardError</code>، و<code dir=\"ltr\">fetch</code> تُرفض حين تنقطع الشبكة. حوّلها مرة واحدة، عند الحافة، إلى الـ <code dir=\"ltr\">Result</code> الذي يتحدّثه كودك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"5\"><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\"> PaymentError</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\"> 'card-declined'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">declineCode</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> chargeCard</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">amount</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:#953800;--shiki-dark:#FFA657\">payment</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentMethod</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Receipt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">PaymentError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</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:#8250DF;--shiki-dark:#D2A8FF\"> ok</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> paymentSdk.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">charge</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(amount, payment));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</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:#0550AE;--shiki-dark:#79C0FF\"> unknown</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\"> (e </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">instanceof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CardError</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\"> err</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ type: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'card-declined'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, declineCode: e.code });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    throw</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> e; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// not a failure we understand: let it propagate</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\">catch</code> من نوع <code dir=\"ltr\">unknown</code>، كما هو دائمًا تحت <code dir=\"ltr\">strict</code>، فتضيّقه بـ <code dir=\"ltr\">instanceof</code> قبل قراءة <code dir=\"ltr\">code</code>. ولا يتحوّل إلى قيمة إلا الإخفاق الوحيد الذي يتوقّعه العمل؛ وكل ما عداه يُعاد رميه دون مساس، مع الـ stack trace كاملًا. ونوع إرجاع الدالة صار الآن قائمة صادقة بما يمكن أن يسوء، يراها كل مستدعٍ في التلميح.</p>\n<p>النتائج غير المتزامنة تتداخل بالطريقة البديهية، <code dir=\"ltr\">Promise&lt;Result&lt;T, E&gt;&gt;</code>. الـ promise المرفوض ما زال يعني “حدث شيء غير متوقَّع”، و<code dir=\"ltr\">{ ok: false }</code> المحلول يعني “العمل قال لا”. وإبقاء هاتين القناتين منفصلتين هو الغاية كلها.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">أين ما زال للاستثناءات مكانها</h2>\n<p><code dir=\"ltr\">Result</code> للإخفاقات التي يتوقّعها العمل ويستطيع المستدعي التصرّف بناءً عليها. إنه ليس بديلًا عن كل <code dir=\"ltr\">throw</code>:</p>\n<ul>\n<li><strong>الأخطاء البرمجية والثوابت المكسورة</strong> (<code dir=\"ltr\">null</code> حيث تقول الأنواع إنه لا يمكن وجوده، أو عضو union غير مُعالَج يصل إلى <code dir=\"ltr\">assertNever</code>) يجب أن ترمي استثناءً. لا يوجد ما هو معقول ليفعله المستدعي، وأنت تريد الـ stack trace.</li>\n<li><strong>أعطال البنية التحتية</strong> التي لا تستطيع معالجتها محليًا (قاعدة البيانات متوقّفة) يمكن أن تنتشر إلى حدّ أخطاء واحد أو طبقة إعادة محاولة.</li>\n<li><strong>كود الأطراف الثالثة الذي يرمي استثناءات</strong> يُغلَّف مرة واحدة عند الحدود، فتتحوّل الاستثناءات التي تتوقّعها إلى <code dir=\"ltr\">err(…)</code> ويُعاد رمي الباقي.</li>\n</ul>\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>التقاط كل شيء في Result</span></p>\n<p><code dir=\"ltr\">try { … } catch (e) { return err({ type: 'unknown', message: String(e) }) }</code> حول دالة كاملة يحوّل الأخطاء البرمجية إلى إخفاقات “متوقَّعة” تعرضها الواجهة بأدب ولا يحقّق فيها أحد. حوّل فقط الإخفاقات المحدّدة التي تفهمها، مثل خطأ الرفض من الـ SDK الخاص بالدفع، ودع الباقي يُرمى.</p>\n</aside>\n<p>في التمرين ستبني خطوة الكوبون في Cartwheel بـ <code dir=\"ltr\">Result</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":2},{"id":"sec-5","text":"أين ما زال للاستثناءات مكانها","level":2}],"takeaways":["توقيع الدالة لا يقول شيئًا عمّا ترميه، لذا يسهل على المستدعين نسيان الإخفاقات المتوقَّعة المخبّأة في الاستثناءات.","<code dir=\"ltr\">type Result&lt;T, E&gt; = { ok: true; value: T } | { ok: false; error: E }</code> هو discriminated union، فعلى المستدعين فحص <code dir=\"ltr\">ok</code> قبل أن يستطيعوا قراءة <code dir=\"ltr\">value</code>.","نمذج الأخطاء أيضًا كـ discriminated union، كي يحمل كل إخفاق بياناته الخاصة وتستطيع الواجهة معالجة كل حالة بشمول.","ركّب النتائج بالعودة المبكرة (<code dir=\"ltr\">if (!r.ok) return r;</code>)؛ فالإخفاق المضيَّق قابل للإسناد إلى union الأخطاء الأوسع لدى المستدعي.","استخدم <code dir=\"ltr\">Result</code> للإخفاقات التي يتوقّعها العمل؛ واحتفظ بـ <code dir=\"ltr\">throw</code> للأخطاء البرمجية والثوابت المكسورة وأعطال البنية التحتية التي لا يمكنك معالجتها محليًا."],"further":[{"title":"Discriminated unions (Narrowing)","url":"https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions"},{"title":"Control flow and error handling (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling"}],"quiz":[{"q":"<p>يمكن أن تفشل <code dir=\"ltr\">placeOrder(cart): Promise&lt;Order&gt;</code> ببطاقة مرفوضة. ماذا يقول توقيعها للمستدعي عن ذلك؟</p>","options":[{"text":"لا شيء؛ لا يملك TypeScript طريقة للإعلان عمّا ترميه الدالة، فيكون الإخفاق غير مرئي.","why":"صحيح. لا توجد جملة <code dir=\"ltr\">throws</code> في TypeScript، ولهذا تنتمي الإخفاقات المتوقَّعة إلى نوع الإرجاع."},{"text":"أنها تُرفض بـ <code dir=\"ltr\">Error</code>، لأن كل promise يمكن أن يُرفض.","why":"أي promise يمكن أن يُرفض بأي قيمة. التوقيع لا يقول أيّ الإخفاقات متوقَّعة ولا ما البيانات التي تحملها."},{"text":"أن على المستدعي تغليفها بـ try/catch، وهذا ما يفرضه <code dir=\"ltr\">strict</code>.","why":"لا يوجد خيار في الـ compiler يفرض try/catch؛ ونسيانها يمرّ في الترجمة بلا مشكلة."},{"text":"أنها قد ترمي <code dir=\"ltr\">CardDeclinedError</code>، مستنتَجًا من التنفيذ.","why":"الأنواع المرمية لا تُستنتج ولا تُتتبّع أبدًا، حتى مع تفعيل كل الخيارات الصارمة."}],"answer":0},{"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\"> Result</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\">E</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\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</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:#953800;--shiki-dark:#FFA657\"> T</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\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">error</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<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\"> r</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">CheckoutError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span>\n<span class=\"line\"><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\">(r.value.id);</span></span></code></pre></div>\n","options":[{"text":"لأن <code dir=\"ltr\">value</code> اختيارية في <code dir=\"ltr\">Result</code>.","why":"<code dir=\"ltr\">value</code> إلزامية في عضو النجاح. المشكلة أنها غير موجودة إطلاقًا في عضو الإخفاق."},{"text":"لأنه يجب انتظار <code dir=\"ltr\">Result</code> بـ await أولًا.","why":"<code dir=\"ltr\">r</code> ليست promise في هذا المقطع؛ لا شيء يحتاج إلى انتظار."},{"text":"لأن <code dir=\"ltr\">value</code> لا توجد إلا في العضو <code dir=\"ltr\">ok: true</code>، ولم يُضيَّق <code dir=\"ltr\">r</code> بعد.","why":"صحيح. فحص <code dir=\"ltr\">if (r.ok)</code> يضيّق <code dir=\"ltr\">r</code> إلى عضو النجاح، حيث تكون <code dir=\"ltr\">value</code> متاحة."}],"answer":2},{"q":"<p>أيّ إخفاق يناسب <code dir=\"ltr\">Result</code> بدل الاستثناء؟</p>","options":[{"text":"وصول <code dir=\"ltr\">null</code> إلى دالة تقول أنواعها إنها لا يمكن أن تكون <code dir=\"ltr\">null</code>.","why":"هذا ثابت مكسور، أي خطأ برمجي. ارمِ استثناءً، وأصلح الكود الذي سمح بحدوثه."},{"text":"نفاد مجمّع الاتصالات بقاعدة البيانات.","why":"عطل في البنية التحتية لا يستطيع نظام الدفع معالجته محليًا؛ دعه ينتشر إلى حدّ الأخطاء أو طبقة إعادة المحاولة."},{"text":"انتهاء صلاحية رمز الكوبون الذي كتبه العميل.","why":"صحيح. إنه متوقَّع، ويستطيع المستدعي فعل شيء مفيد (عرض رسالة)، ويحمل بيانات (الرمز وتاريخ انتهائه)."}],"answer":2},{"q":"<p>تُعيد <code dir=\"ltr\">validateCart</code> النوع <code dir=\"ltr\">Result&lt;Cart, CartError&gt;</code> وتُعيد <code dir=\"ltr\">checkout</code> النوع <code dir=\"ltr\">Result&lt;Order, CartError | PaymentError&gt;</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> cart</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateCart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input);</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\">cart.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cart;</span></span></code></pre></div>\n","options":[{"text":"لا، لأن <code dir=\"ltr\">Result&lt;Cart, CartError&gt;</code> ليس <code dir=\"ltr\">Result&lt;Order, …&gt;</code>.","why":"بعد الفحص، يكون <code dir=\"ltr\">cart</code> عضو الإخفاق فقط، <code dir=\"ltr\">{ ok: false; error: CartError }</code>، وليس فيه <code dir=\"ltr\">Cart</code>."},{"text":"لا، يجب إعادة بناء الإخفاق بـ <code dir=\"ltr\">err(cart.error)</code>.","why":"إعادة البناء تعمل، لكنها غير ضرورية؛ الإخفاق المضيَّق مناسب أصلًا."},{"text":"فقط مع تحويل <code dir=\"ltr\">as</code>.","why":"لا حاجة إلى تحويل؛ قابلية الإسناد تتكفّل بذلك."},{"text":"نعم، لأن <code dir=\"ltr\">{ ok: false; error: CartError }</code> المضيَّق قابل للإسناد إلى عضو الإخفاق الأوسع.","why":"صحيح. <code dir=\"ltr\">CartError</code> جزء من <code dir=\"ltr\">CartError | PaymentError</code>، فيمكن إعادة الإخفاق كما هو."}],"answer":3}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type Coupon = { code: string; percentOff: number; expiresOn: string };\ntype Cart = { lines: { sku: string; quantity: number; unitPriceCents: number }[] };\ntype PricedCart = Cart & { totalCents: number };\n\nconst COUPONS: Coupon[] = [\n  { code: 'WELCOME10', percentOff: 10, expiresOn: '2099-12-31' },\n  { code: 'SPRING15', percentOff: 15, expiresOn: '2025-05-31' },\n];\nconst STOCK: Record<string, number> = { 'TENT-2P': 4, 'MUG-SET': 0 };\n\n// 1. Define Result, ok and err.\ntype Result<T, E> = unknown;\n\ntype CouponError =\n  | { type: 'coupon-unknown'; code: string }\n  | { type: 'coupon-expired'; code: string; expiredOn: string };\ntype StockError = { type: 'out-of-stock'; sku: string; available: number };\ntype CheckoutError = CouponError | StockError;\n\n// 2. Return a Result instead of throwing.\nfunction findCoupon(code: string, today: string): Coupon {\n  const coupon = COUPONS.find((c) => c.code === code);\n  if (!coupon) throw new Error(`Unknown coupon ${code}`);\n  if (coupon.expiresOn < today) throw new Error(`${code} expired on ${coupon.expiresOn}`);\n  return coupon;\n}\n\nfunction checkStock(cart: Cart): Result<Cart, StockError> {\n  for (const line of cart.lines) {\n    const available = STOCK[line.sku] ?? 0;\n    if (available < line.quantity) return { ok: false, error: { type: 'out-of-stock', sku: line.sku, available } };\n  }\n  return { ok: true, value: cart };\n}\n\n// 3. Compose: stock first, then the coupon (if any), then price the cart.\nfunction checkout(cart: Cart, couponCode: string | null, today: string) {\n  const subtotal = cart.lines.reduce((sum, l) => sum + l.quantity * l.unitPriceCents, 0);\n  const coupon = couponCode ? findCoupon(couponCode, today) : null;\n  const totalCents = coupon ? Math.round(subtotal * (1 - coupon.percentOff / 100)) : subtotal;\n  return { ok: true, value: { ...cart, totalCents } };\n}\n\n// 4. One message per error type.\nfunction errorMessage(e: CheckoutError): string {\n  return 'Something went wrong';\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<ReturnType<typeof findCoupon>, Result<Coupon, CouponError>>>;\ntype T2 = Expect<Equal<ReturnType<typeof checkout>, Result<PricedCart, CheckoutError>>>;\n\nfunction typeTests(r: Result<Coupon, CouponError>) {\n  // @ts-expect-error: value is only available after checking ok\n  r.value.code;\n  if (r.ok) r.value.code;\n}\n\nconst cart: Cart = { lines: [{ sku: 'TENT-2P', quantity: 1, unitPriceCents: 18900 }] };\nconst outcome = checkout(cart, 'SPRING15', '2026-10-05');\nconsole.log(outcome.ok ? outcome.value.totalCents : errorMessage(outcome.error));\n"},"solution":{"ts":"type Coupon = { code: string; percentOff: number; expiresOn: string };\ntype Cart = { lines: { sku: string; quantity: number; unitPriceCents: number }[] };\ntype PricedCart = Cart & { totalCents: number };\n\nconst COUPONS: Coupon[] = [\n  { code: 'WELCOME10', percentOff: 10, expiresOn: '2099-12-31' },\n  { code: 'SPRING15', percentOff: 15, expiresOn: '2025-05-31' },\n];\nconst STOCK: Record<string, number> = { 'TENT-2P': 4, 'MUG-SET': 0 };\n\ntype Result<T, E> = { ok: true; value: T } | { ok: false; error: E };\nconst ok = <T>(value: T): { ok: true; value: T } => ({ ok: true, value });\nconst err = <E>(error: E): { ok: false; error: E } => ({ ok: false, error });\n\ntype CouponError =\n  | { type: 'coupon-unknown'; code: string }\n  | { type: 'coupon-expired'; code: string; expiredOn: string };\ntype StockError = { type: 'out-of-stock'; sku: string; available: number };\ntype CheckoutError = CouponError | StockError;\n\nfunction findCoupon(code: string, today: string): Result<Coupon, CouponError> {\n  const coupon = COUPONS.find((c) => c.code === code);\n  if (!coupon) return err({ type: 'coupon-unknown', code });\n  if (coupon.expiresOn < today) return err({ type: 'coupon-expired', code, expiredOn: coupon.expiresOn });\n  return ok(coupon);\n}\n\nfunction checkStock(cart: Cart): Result<Cart, StockError> {\n  for (const line of cart.lines) {\n    const available = STOCK[line.sku] ?? 0;\n    if (available < line.quantity) return { ok: false, error: { type: 'out-of-stock', sku: line.sku, available } };\n  }\n  return { ok: true, value: cart };\n}\n\nfunction checkout(cart: Cart, couponCode: string | null, today: string): Result<PricedCart, CheckoutError> {\n  const stocked = checkStock(cart);\n  if (!stocked.ok) return stocked;\n\n  const subtotal = cart.lines.reduce((sum, l) => sum + l.quantity * l.unitPriceCents, 0);\n  if (couponCode === null) return ok({ ...cart, totalCents: subtotal });\n\n  const coupon = findCoupon(couponCode, today);\n  if (!coupon.ok) return coupon;\n  return ok({ ...cart, totalCents: Math.round(subtotal * (1 - coupon.value.percentOff / 100)) });\n}\n\nfunction errorMessage(e: CheckoutError): string {\n  switch (e.type) {\n    case 'coupon-unknown': return `We don't recognise the code ${e.code}.`;\n    case 'coupon-expired': return `${e.code} expired on ${e.expiredOn}.`;\n    case 'out-of-stock': return `Only ${e.available} of ${e.sku} left in stock.`;\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<ReturnType<typeof findCoupon>, Result<Coupon, CouponError>>>;\ntype T2 = Expect<Equal<ReturnType<typeof checkout>, Result<PricedCart, CheckoutError>>>;\n\nfunction typeTests(r: Result<Coupon, CouponError>) {\n  // @ts-expect-error: value is only available after checking ok\n  r.value.code;\n  if (r.ok) r.value.code;\n}\n\nconst cart: Cart = { lines: [{ sku: 'TENT-2P', quantity: 1, unitPriceCents: 18900 }] };\nconst outcome = checkout(cart, 'SPRING15', '2026-10-05');\nconsole.log(outcome.ok ? outcome.value.totalCents : errorMessage(outcome.error));\n"},"checks":[{"id":"types","typecheck":true},{"id":"valid-coupon","test":"const r = checkout({ lines: [{ sku: 'TENT-2P', quantity: 1, unitPriceCents: 10000 }] }, 'WELCOME10', '2026-10-05'); return r.ok === true && r.value.totalCents === 9000;"},{"id":"expired","test":"const r = checkout({ lines: [{ sku: 'TENT-2P', quantity: 1, unitPriceCents: 10000 }] }, 'SPRING15', '2026-10-05'); return r.ok === false && r.error.type === 'coupon-expired' && r.error.expiredOn === '2025-05-31';"},{"id":"stock-first","test":"const r = checkout({ lines: [{ sku: 'MUG-SET', quantity: 1, unitPriceCents: 2500 }] }, 'NOPE', '2026-10-05'); return r.ok === false && r.error.type === 'out-of-stock';"},{"id":"messages","test":"return errorMessage({ type: 'coupon-unknown', code: 'NOPE' }).includes('NOPE') && errorMessage({ type: 'out-of-stock', sku: 'MUG-SET', available: 0 }).includes('MUG-SET') && errorMessage({ type: 'coupon-expired', code: 'SPRING15', expiredOn: '2025-05-31' }).includes('2025-05-31');"}],"title":"أعِد الإخفاقات، لا ترمِها","prompt":"<p>حوّل خطوة الكوبون في Cartwheel إلى إخفاقات متوقَّعة محدَّدة الأنواع:</p>\n<ol>\n<li>عرّف <code dir=\"ltr\">Result&lt;T, E&gt;</code> كـ discriminated union على <code dir=\"ltr\">ok</code>، مع الدالتين المساعدتين <code dir=\"ltr\">ok(value)</code> و<code dir=\"ltr\">err(error)</code>.</li>\n<li>اجعل <code dir=\"ltr\">findCoupon</code> تُعيد <code dir=\"ltr\">Result&lt;Coupon, CouponError&gt;</code>: <code dir=\"ltr\">coupon-unknown</code> حين لا يوجد الرمز، و<code dir=\"ltr\">coupon-expired</code> (مع <code dir=\"ltr\">expiredOn</code>) حين يكون <code dir=\"ltr\">expiresOn</code> قبل <code dir=\"ltr\">today</code>.</li>\n<li>اجعل <code dir=\"ltr\">checkout</code> تُعيد <code dir=\"ltr\">Result&lt;PricedCart, CheckoutError&gt;</code>: افحص المخزون أولًا، ثم الكوبون (حين لا يكون <code dir=\"ltr\">couponCode</code> هو <code dir=\"ltr\">null</code>)، ثم أعِد السلة مع <code dir=\"ltr\">totalCents</code> (المجموع الفرعي ناقص نسبة الكوبون، مقرّبًا).</li>\n<li>اجعل <code dir=\"ltr\">errorMessage</code> تُعيد رسالة لكل نوع خطأ تتضمّن بياناته (الرمز، أو تاريخ الانتهاء، أو رمز المنتج SKU).</li>\n</ol>\n","hints":["<code dir=\"ltr\">type Result&lt;T, E&gt; = { ok: true; value: T } | { ok: false; error: E };</code> و<code dir=\"ltr\">const ok = &lt;T&gt;(value: T): { ok: true; value: T } =&gt; ({ ok: true, value });</code>.","في <code dir=\"ltr\">checkout</code>، استخدم العودة المبكرة: <code dir=\"ltr\">const stocked = checkStock(cart); if (!stocked.ok) return stocked;</code>. الإخفاق المضيَّق يناسب الـ union الأوسع <code dir=\"ltr\">CheckoutError</code>.","علّق نوع إرجاع <code dir=\"ltr\">checkout</code> صراحة، وتفرّع على <code dir=\"ltr\">e.type</code> في <code dir=\"ltr\">errorMessage</code> مع نوع إرجاع <code dir=\"ltr\">: string</code> كي تكون الحالة المنسيّة خطأ ترجمة."],"explanation":"<p>حين يكون <code dir=\"ltr\">Result</code> هو discriminated union، فإن <code dir=\"ltr\">value</code> ببساطة غير موجودة حتى يفحص المستدعي <code dir=\"ltr\">ok</code>، وهذا ما يثبته سطر <code dir=\"ltr\">@ts-expect-error</code>. صارت <code dir=\"ltr\">findCoupon</code> تعلن إخفاقَيها المتوقَّعين في توقيعها بدل إخفائهما في <code dir=\"ltr\">throw</code>. والعودة المبكرة تُبقي <code dir=\"ltr\">checkout</code> مسطّحة: كل إخفاق مضيَّق قابل للإسناد إلى <code dir=\"ltr\">CheckoutError</code>، فيُعاد دون تغيير. ولأن الأخطاء union بمميِّز <code dir=\"ltr\">type</code>، تستطيع <code dir=\"ltr\">errorMessage</code> استخدام بيانات كل إخفاق، وإضافة نوع خطأ جديد لاحقًا تعلّم هذه الدالة تلقائيًا.</p>\n","checkLabels":{"types":"الملف يجتاز فحص الأنواع بلا أي خطأ، بما فيها كل اختبارات الأنواع","valid-coupon":"الكوبون الصالح يخفّض المجموع (خصم 10% من 10000 يعطي 9000)","expired":"الكوبون المنتهي يُعيد إخفاق <code dir=\"ltr\">coupon-expired</code> مع تاريخ انتهائه","stock-first":"يُفحص المخزون قبل الكوبون","messages":"<code dir=\"ltr\">errorMessage</code> تتضمّن بيانات كل إخفاق"}},"words":632,"readMinutes":4}