{"id":"l-at-1-3","courseId":"advanced-typescript","lang":"ar","kind":"lesson","summary":"استخدم typeof والمساواة وin وinstanceof والعودة المبكرة لتضييق الأنواع الواسعة، وتعرّف على المواضع التي ينسى فيها TypeScript الـ narrowing كي تحافظ عليه.","html":"<p>لطلب Cartwheel الحقل <code dir=\"ltr\">shippingFeeCents: number | null</code>. القيمة <code dir=\"ltr\">null</code> تعني أن الرسوم غير معروفة بعد (العنوان غير مكتمل). و<code dir=\"ltr\">0</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\"> shippingLabel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">feeCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</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\">  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\">feeCents) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Calculated at checkout'</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\"> (feeCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\"> 'Free'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `$${</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">feeCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toFixed</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>كل عميل شحنه مجاني رأى “Calculated at checkout”. لم يشتكِ TypeScript، لأن <code dir=\"ltr\">feeCents</code> بعد السطر الأول من نوع <code dir=\"ltr\">number</code>، ومقارنة رقم بـ <code dir=\"ltr\">0</code> أمر مشروع تمامًا. الأنواع كانت صحيحة؛ الـ narrowing (تضييق النوع) هو الخاطئ. هذا الدرس عن كيفية تضييق TypeScript للأنواع، كي تجعله يعمل لصالحك بدل أن تثق به ثقة عمياء.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">تحليل تدفّق التحكّم</h2>\n<p>يتتبّع TypeScript نوع كل متغيّر عبر كل فرع وكل جملة عودة. يمكن أن يكون للاسم نفسه نوع مختلف في كل سطر:</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\"> shippingLabel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">feeCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</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:#6E7781;--shiki-dark:#8B949E\">  // feeCents: number | null</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (feeCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</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\"> 'Calculated at checkout'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // feeCents: number</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (feeCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\"> 'Free'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // feeCents: number</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `$${</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">feeCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toFixed</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>هذا هو <strong>تحليل تدفّق التحكّم (control-flow analysis)</strong>. كل فحص يقسم التدفّق إلى فروع، وفي كل فرع يكون نوع المتغيّر هو مجموعة القيم التي يمكن أن تبقى هناك. وحين يعود فرعٌ، لا يرى الكود بعده إلا ما تبقّى.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 300\" role=\"img\" aria-labelledby=\"l-at-1-3-t1\"><defs><marker id=\"arrow-l-at-1-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-at-1-3-t1\">تدفّق التحكّم في shippingLabel. يبدأ المعامل بنوع number أو null. الفحص feeCents يساوي null يتفرّع إلى جملة عودة نوعها null. المسار المتبقّي نوعه number، ثم الفحص feeCents يساوي 0 يتفرّع إلى جملة عودة للشحن المجاني، والمسار الأخير رسوم موجبة من نوع number.</title>\n  <rect class=\"d-box-accent\" x=\"230\" y=\"14\" width=\"220\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"42\" text-anchor=\"middle\">number | null</text>\n  <path class=\"d-arrow\" d=\"M340 58 L340 92\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <rect class=\"d-box-warn\" x=\"230\" y=\"96\" width=\"220\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"121\" text-anchor=\"middle\">feeCents === null ?</text>\n  <path class=\"d-arrow\" d=\"M230 116 L110 116 L110 150\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"160\" y=\"108\" text-anchor=\"middle\">نعم</text>\n  <rect class=\"d-box\" x=\"20\" y=\"154\" width=\"180\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"110\" y=\"181\" text-anchor=\"middle\">null → \"Calculated…\"</text>\n  <path class=\"d-arrow\" d=\"M340 136 L340 170\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"356\" y=\"158\">لا</text>\n  <rect class=\"d-box-warn\" x=\"230\" y=\"174\" width=\"220\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"199\" text-anchor=\"middle\">number: === 0 ?</text>\n  <path class=\"d-arrow\" d=\"M450 194 L570 194 L570 228\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"510\" y=\"186\" text-anchor=\"middle\">نعم</text>\n  <rect class=\"d-box\" x=\"480\" y=\"232\" width=\"180\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"570\" y=\"259\" text-anchor=\"middle\">0 → \"Free\"</text>\n  <path class=\"d-arrow\" d=\"M340 214 L340 248\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"230\" y=\"252\" width=\"220\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"277\" text-anchor=\"middle\">number → \"$4.99\"</text>\n</svg><figcaption>كل فحص يقسم التدفّق؛ والنوع بعده هو ما يمكن أن يبقى</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">صندوق أدوات الـ narrowing</h2>\n<p>تضيّق الأنواع بفحوص JavaScript عادية؛ ويفهم TypeScript كل واحد منها:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>الفحص</th>\n<th>ما يضيّقه</th>\n<th>مثال</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">typeof x === 'string'</code></td>\n<td>الأنواع الأولية</td>\n<td><code dir=\"ltr\">typeof amount === 'number'</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">x === null</code>، <code dir=\"ltr\">x !== undefined</code></td>\n<td>القيم الحرفية</td>\n<td><code dir=\"ltr\">coupon !== null</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">'prop' in x</code></td>\n<td>unions الكائنات حسب الخاصية</td>\n<td><code dir=\"ltr\">'trackingNumber' in shipment</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">x instanceof C</code></td>\n<td>نُسخ الأصناف</td>\n<td><code dir=\"ltr\">err instanceof TypeError</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">Array.isArray(x)</code></td>\n<td>المصفوفات</td>\n<td><code dir=\"ltr\">Array.isArray(input)</code></td>\n</tr>\n</tbody>\n</table></div><p>فحص الصدقية (truthiness)، أي <code dir=\"ltr\">if (x)</code>، يضيّق النوع أيضًا، وهو الفحص الذي يجب أن تحذر منه. فهو يُزيل <code dir=\"ltr\">null</code> و<code dir=\"ltr\">undefined</code>، لكنه يُزيل أيضًا <code dir=\"ltr\">0</code> و<code dir=\"ltr\">''</code> و<code dir=\"ltr\">NaN</code> و<code dir=\"ltr\">false</code>. مع كائن أو مصفوفة، فحص الصدقية مقبول. أما مع رقم أو نص يعني فيه الصفر أو الفراغ شيئًا، فقارن مع <code dir=\"ltr\">null</code> صراحةً.</p>\n<h3 id=\"sec-3\" tabindex=\"-1\"><code dir=\"ltr\">in</code> و<code dir=\"ltr\">instanceof</code> عمليًا</h3>\n<p>فحصان يستحقان نظرة أقرب لأن عميل الدفع يستخدمهما باستمرار. يجيب مزوّد الدفع بأحد شكلين، وواحد منهما فقط فيه <code dir=\"ltr\">receiptId</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentReply</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">receiptId</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\">capturedCents</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:#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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> summarize</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">reply</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentReply</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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'receiptId'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> reply) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `Paid, receipt ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">reply</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">receiptId</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:#0A3069;--shiki-dark:#A5D6FF\"> `Declined (${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">reply</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">'receiptId' in reply</code> تُبقي فقط أعضاء الـ union التي تعرّف <code dir=\"ltr\">receiptId</code>. هذا يعمل، لكنه يضيّق النوع بناءً على <em>غياب</em> حقل في الأعضاء الأخرى، فإضافة <code dir=\"ltr\">receiptId?</code> إلى شكل الرفض لاحقًا ستكسره بصمت. حين تتحكّم في الأنواع، يكون المميِّز الحرفي (الدرس القادم) أمتن.</p>\n<p>و<code dir=\"ltr\">instanceof</code> هي أداة الأخطاء. تحت <code dir=\"ltr\">strict</code>، يكون المتغيّر في جملة <code dir=\"ltr\">catch</code> من نوع <code dir=\"ltr\">unknown</code>، لأن JavaScript تسمح لك برمي (<code dir=\"ltr\">throw</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> describeFailure</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">err</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 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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (err </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">instanceof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> err.message;</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\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> err </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'string'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> err;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Unknown error'</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<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\">if (!order.discount)</code> و<code dir=\"ltr\">if (!feeCents)</code> و<code dir=\"ltr\">if (!couponCode)</code> كلها تعامل <code dir=\"ltr\">0</code> أو <code dir=\"ltr\">''</code> ذات المعنى على أنها “مفقودة”. ولن يحذّرك TypeScript، لأن القراءتين كلتيهما صحيحتان من حيث النوع. حين يمكن أن تكون القيمة صفرًا أو فارغة بشكل مشروع، اكتب <code dir=\"ltr\">=== null</code> أو <code dir=\"ltr\">=== undefined</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">أين يضيع الـ narrowing</h2>\n<p>الـ narrowing حقيقةٌ عن نقطة واحدة في التدفّق. والـ callback يُنفَّذ في وقت آخر، لذا يحذر TypeScript مما يحمله إلى داخله:</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\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">couponCode</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</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\"> couponBanner</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:#953800;--shiki-dark:#FFA657\">names</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[])</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (order.couponCode </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</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\"> names.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</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\">order</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">couponCode</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toUpperCase</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} is applied`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // Error: 'order.couponCode' is possibly 'null'.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>خارج الدالة السهمية، <code dir=\"ltr\">order.couponCode</code> من نوع <code dir=\"ltr\">string</code>. وداخلها يختفي الـ narrowing، لأن <code dir=\"ltr\">couponCode</code> خاصية قابلة للتغيير وقد يعيدها شيءٌ إلى <code dir=\"ltr\">null</code> قبل أن تستدعي <code dir=\"ltr\">map</code> الـ callback. والإصلاح هو التقاط القيمة المضيَّقة في <code dir=\"ltr\">const</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> couponBanner</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:#953800;--shiki-dark:#FFA657\">names</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[])</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[] {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> code</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order.couponCode;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</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\"> names.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</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\">code</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toUpperCase</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} is applied`</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>منذ TypeScript 5.4، تحتفظ المعاملات ومتغيّرات <code dir=\"ltr\">let</code> أيضًا بتضييقها داخل الـ callbacks، ما دام لا يُعاد الإسناد إليها بعد إنشاء الـ callback. أما الخصائص فلا تحتفظ به أبدًا. النسخ إلى <code dir=\"ltr\">const</code> يعمل في كل الإصدارات ويجعل النية واضحة.</p>\n<aside class=\"callout callout-note\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">ملاحظة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>TypeScript متفائل تجاه استدعاءات الدوال</span></p>\n<p>الحالة المعاكسة مسموحة: بعد <code dir=\"ltr\">if (order.couponCode !== null) { applyDiscount(order); … }</code>، ما زال TypeScript يعامل <code dir=\"ltr\">order.couponCode</code> على أنه <code dir=\"ltr\">string</code>، مع أن <code dir=\"ltr\">applyDiscount</code> ربما غيّرته. إعادة الفحص بعد كل استدعاء كانت ستجعل الـ narrowing بلا فائدة، لذا يثق بك TypeScript هنا. تجنّب الدوال التي تعدّل وسائطها ولن يلسعك هذا أبدًا.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">تضييق المصفوفات بـ filter</h2>\n<p>كانت <code dir=\"ltr\">filter</code> مصدر إحباط كلاسيكيًا: <code dir=\"ltr\">coupons.filter((c) =&gt; c !== null)</code> كان نوعها يبقى <code dir=\"ltr\">(string | null)[]</code>. منذ TypeScript 5.5، <strong>يستنتج TypeScript الـ type predicate</strong> من الدوال السهمية البسيطة مثل هذه، فتكون النتيجة <code dir=\"ltr\">string[]</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"6\"><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\"> coupons</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</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:#0A3069;--shiki-dark:#A5D6FF\">'WELCOME10'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'LOYAL5'</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\"> codes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupons.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</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:#1F2328;--shiki-dark:#E6EDF3\"> c </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// string[]</span></span></code></pre></div>\n<p>لا يحدث الاستنتاج إلا حين يثبت الفحصُ النوعَ في الاتجاهين. <code dir=\"ltr\">(c) =&gt; !!c</code> لا تُستنتج على أنها predicate: فالنتيجة الكاذبة قد تعني <code dir=\"ltr\">''</code> لا <code dir=\"ltr\">null</code>، فلا يستطيع TypeScript أن يستنتج أن القيمة المرفوضة هي <code dir=\"ltr\">null</code>. والأمر نفسه مع <code dir=\"ltr\">filter(Boolean)</code>. اكتب المقارنة الصريحة وستحصل على الـ narrowing وعلى التعامل الصحيح مع النصوص الفارغة معًا.</p>\n<p>التمرين أدناه يجمع الأخطاء الثلاثة من هذا الدرس في ملف واحد. وفي الدرس التالي ستصمّم أنواعًا تجعل الـ narrowing شبه تلقائي: الـ discriminated unions.</p>\n","toc":[{"id":"sec-1","text":"تحليل تدفّق التحكّم","level":2},{"id":"sec-2","text":"صندوق أدوات الـ narrowing","level":2},{"id":"sec-3","text":"in وinstanceof عمليًا","level":3},{"id":"sec-4","text":"أين يضيع الـ narrowing","level":2},{"id":"sec-5","text":"تضييق المصفوفات بـ filter","level":2}],"takeaways":["يمنح تحليل تدفّق التحكّم المتغيّرَ نوعًا مختلفًا عند كل نقطة في الكود، بناءً على الفحوص وجمل العودة التي سبقتها.","فحوص الصدقية (truthiness) تُزيل أيضًا <code dir=\"ltr\">0</code> و<code dir=\"ltr\">''</code>، لذا قارن مع <code dir=\"ltr\">null</code> أو <code dir=\"ltr\">undefined</code> صراحةً حين تكون لهاتين القيمتين دلالة.","يسقط الـ narrowing لخاصية قابلة للتغيير داخل الـ callbacks؛ انسخ القيمة إلى <code dir=\"ltr\">const</code> قبل الـ callback لتحافظ عليه.","منذ TypeScript 5.5، تُستنتج الدالة السهمية البسيطة مثل <code dir=\"ltr\">(c) =&gt; c !== null</code> الممرَّرة إلى <code dir=\"ltr\">filter</code> على أنها type predicate، فتضيق النتيجة."],"further":[{"title":"Narrowing","url":"https://www.typescriptlang.org/docs/handbook/2/narrowing.html"},{"title":"TypeScript 5.5 release notes (inferred type predicates)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html"}],"quiz":[{"q":"<p>قيمة <code dir=\"ltr\">shippingFeeCents</code> هي <code dir=\"ltr\">null</code> حين لا تكون الرسوم معروفة بعد، و<code dir=\"ltr\">0</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\">fee</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</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\">fee) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Calculated at checkout'</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\"> fee </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Free'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">fee</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#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":"‘Free’","why":"فرع <code dir=\"ltr\">'Free'</code> لا يمكن الوصول إليه مع <code dir=\"ltr\">0</code>، لأن <code dir=\"ltr\">!0</code> تساوي <code dir=\"ltr\">true</code> فتُنفَّذ جملة العودة الأولى قبله."},{"text":"‘Calculated at checkout’","why":"صحيح. فحص الصدقية يعامل <code dir=\"ltr\">0</code> كما يعامل <code dir=\"ltr\">null</code>. اكتب <code dir=\"ltr\">if (fee === null)</code> بدلًا منه."},{"text":"‘0’","why":"لا تصل الدالة أبدًا إلى النص القالبي مع <code dir=\"ltr\">0</code>؛ فحص الصدقية يعود قبل ذلك."},{"text":"لا يمرّ في الترجمة، لأن <code dir=\"ltr\">fee === 0</code> لا تداخل فيه بعد الفحص.","why":"بعد أن يعود <code dir=\"ltr\">!fee</code>، يبقى <code dir=\"ltr\">fee</code> من نوع <code dir=\"ltr\">number</code>، ومقارنة <code dir=\"ltr\">number</code> بـ <code dir=\"ltr\">0</code> مسموحة. الخطأ صامت."}],"answer":1},{"q":"<p>لماذا يكون <code dir=\"ltr\">order.coupon</code> خطأً داخل الـ callback؟</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\">if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (order.coupon </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  names.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</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\">order</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupon</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toUpperCase</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></code></pre></div>\n","options":[{"text":"لأن callbacks الدالة <code dir=\"ltr\">map</code> لا تُفحص أنواعها تحت <code dir=\"ltr\">strict</code>.","why":"الـ callbacks تُفحص بالكامل. والخطأ موجود تحديدًا لأنها تُفحص."},{"text":"لأن TypeScript لا يمكنه معرفة متى يُنفَّذ الـ callback، و<code dir=\"ltr\">order.coupon</code> خاصية قابلة للتغيير قد تصبح <code dir=\"ltr\">null</code> حينها.","why":"صحيح. تضييق خاصية قابلة للتغيير لا ينتقل إلى دالة أُنشئت داخل الكتلة. انسخها إلى <code dir=\"ltr\">const</code> أولًا."},{"text":"لأن <code dir=\"ltr\">!== null</code> لا تضيّق النوع؛ تحتاج إلى <code dir=\"ltr\">typeof order.coupon === 'string'</code>.","why":"المقارنة مع <code dir=\"ltr\">null</code> تضيّق النوع جيدًا خارج الـ callback. المشكلة في حدود الـ callback، لا في الفحص."}],"answer":1},{"q":"<p>في TypeScript 5.9، ما نوع <code dir=\"ltr\">codes</code>؟</p>","options":[{"text":"<code dir=\"ltr\">string[]</code> حين تُكتب <code dir=\"ltr\">coupons.filter((c) =&gt; c !== null)</code> مع <code dir=\"ltr\">coupons: (string | null)[]</code>","why":"صحيح. يستنتج TypeScript 5.5 وما بعده أن <code dir=\"ltr\">(c) =&gt; c !== null</code> هي الـ predicate <code dir=\"ltr\">c is string</code>، فتضيّق <code dir=\"ltr\">filter</code> المصفوفة."},{"text":"<code dir=\"ltr\">string[]</code> حين تُكتب <code dir=\"ltr\">coupons.filter(Boolean)</code>","why":"<code dir=\"ltr\">Boolean</code> ليست type predicate، وفحص الصدقية لا يمكن أن يكون واحدًا أصلًا لأنه يُزيل <code dir=\"ltr\">''</code> أيضًا. تبقى النتيجة <code dir=\"ltr\">(string | null)[]</code>."},{"text":"<code dir=\"ltr\">string[]</code> حين تُكتب <code dir=\"ltr\">coupons.filter((c) =&gt; !!c)</code>","why":"لا تُستنتج الصدقية على أنها predicate، لأن النتيجة الكاذبة لا تثبت أن القيمة <code dir=\"ltr\">null</code>؛ فقد تكون <code dir=\"ltr\">''</code>."}],"answer":0},{"q":"<p>أيّ فحص يضيّق <code dir=\"ltr\">input: Order | Order[]</code> إلى <code dir=\"ltr\">Order[]</code>؟</p>","options":[{"text":"<code dir=\"ltr\">typeof input === 'array'</code>","why":"<code dir=\"ltr\">typeof</code> لا تُعيد أبدًا <code dir=\"ltr\">'array'</code>؛ مع المصفوفات تُعيد <code dir=\"ltr\">'object'</code>، ويعلّم TypeScript هذه المقارنة."},{"text":"<code dir=\"ltr\">input instanceof Object</code>","why":"كلٌّ من <code dir=\"ltr\">Order</code> والمصفوفة كائنات، فلا يستطيع هذا الفحص التمييز بينهما."},{"text":"<code dir=\"ltr\">'length' in input</code>","why":"هذا يضيّق النوع فقط إن لم يكن لـ <code dir=\"ltr\">Order</code> خاصية <code dir=\"ltr\">length</code>، وهذا هشّ؛ الفحص المخصَّص أوضح وأأمن."},{"text":"<code dir=\"ltr\">Array.isArray(input)</code>","why":"صحيح. <code dir=\"ltr\">Array.isArray</code> معرَّفة كـ type predicate، فيرى الفرع الصادق <code dir=\"ltr\">Order[]</code> ويرى الفرع الكاذب <code dir=\"ltr\">Order</code>."}],"answer":3}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type Order = {\n  id: number;\n  couponCode: string | null;\n  // null = not known yet (address incomplete); 0 = free shipping\n  shippingFeeCents: number | null;\n};\n\nfunction shippingLabel(order: Order): string {\n  if (!order.shippingFeeCents) return 'Calculated at checkout';\n  if (order.shippingFeeCents === 0) return 'Free';\n  return `$${(order.shippingFeeCents / 100).toFixed(2)}`;\n}\n\nfunction usedCoupons(orders: Order[]): string[] {\n  return orders.map((o) => o.couponCode).filter((c) => c);\n}\n\nfunction couponBanner(order: Order, names: string[]): string[] {\n  if (order.couponCode === null) return [];\n  return names.map((n) => `${n}, ${order.couponCode.toUpperCase()} is applied`);\n}\n\nconst orders: Order[] = [\n  { id: 1001, couponCode: 'welcome10', shippingFeeCents: 0 },\n  { id: 1002, couponCode: null, shippingFeeCents: null },\n  { id: 1003, couponCode: 'loyal5', shippingFeeCents: 499 },\n];\nconsole.log(orders.map(shippingLabel), usedCoupons(orders));\n"},"solution":{"ts":"type Order = {\n  id: number;\n  couponCode: string | null;\n  // null = not known yet (address incomplete); 0 = free shipping\n  shippingFeeCents: number | null;\n};\n\nfunction shippingLabel(order: Order): string {\n  const fee = order.shippingFeeCents;\n  if (fee === null) return 'Calculated at checkout';\n  if (fee === 0) return 'Free';\n  return `$${(fee / 100).toFixed(2)}`;\n}\n\nfunction usedCoupons(orders: Order[]): string[] {\n  return orders.map((o) => o.couponCode).filter((c) => c !== null);\n}\n\nfunction couponBanner(order: Order, names: string[]): string[] {\n  const code = order.couponCode;\n  if (code === null) return [];\n  return names.map((n) => `${n}, ${code.toUpperCase()} is applied`);\n}\n\nconst orders: Order[] = [\n  { id: 1001, couponCode: 'welcome10', shippingFeeCents: 0 },\n  { id: 1002, couponCode: null, shippingFeeCents: null },\n  { id: 1003, couponCode: 'loyal5', shippingFeeCents: 499 },\n];\nconsole.log(orders.map(shippingLabel), usedCoupons(orders));\n"},"checks":[{"id":"types","typecheck":true},{"id":"free-shipping","test":"const o = (fee) => ({ id: 1, couponCode: null, shippingFeeCents: fee }); return shippingLabel(o(0)) === 'Free' && shippingLabel(o(null)) === 'Calculated at checkout' && shippingLabel(o(499)) === '$4.99';"},{"id":"used-coupons","test":"const r = usedCoupons([{ id: 1, couponCode: 'A', shippingFeeCents: 0 }, { id: 2, couponCode: null, shippingFeeCents: 0 }, { id: 3, couponCode: 'B', shippingFeeCents: 0 }]); return JSON.stringify(r) === '[\"A\",\"B\"]';"},{"id":"banner","test":"const r = couponBanner({ id: 1, couponCode: 'loyal5', shippingFeeCents: 0 }, ['Mona']); return r.length === 1 && r[0].includes('LOYAL5') && couponBanner({ id: 2, couponCode: null, shippingFeeCents: 0 }, ['Mona']).length === 0;"}],"title":"أصلح ثلاثة أخطاء في الـ narrowing","prompt":"<p>في هذا الملف مشكلات الـ narrowing الثلاث من الدرس. أصلحها دون استخدام تأكيد عدم الفراغ <code dir=\"ltr\">!</code> (non-null assertion) أو <code dir=\"ltr\">as</code> أو <code dir=\"ltr\">any</code>:</p>\n<ol>\n<li>يجب أن تُعيد <code dir=\"ltr\">shippingLabel</code> القيمة <code dir=\"ltr\">'Free'</code> حين تكون <code dir=\"ltr\">shippingFeeCents</code> تساوي <code dir=\"ltr\">0</code>، و<code dir=\"ltr\">'Calculated at checkout'</code> حين تكون <code dir=\"ltr\">null</code>، ومبلغًا بالدولار مثل <code dir=\"ltr\">'$4.99'</code> في غير ذلك.</li>\n<li>يجب أن تُعيد <code dir=\"ltr\">usedCoupons</code> مصفوفة <code dir=\"ltr\">string[]</code> حقيقية من رموز الكوبونات غير الفارغة.</li>\n<li>يجب أن تمرّ <code dir=\"ltr\">couponBanner</code> في الترجمة وأن تبقى تحوّل الرمز إلى أحرف كبيرة داخل الـ callback.</li>\n</ol>\n","hints":["في 1، قارن مع <code dir=\"ltr\">null</code> بدل الاعتماد على الصدقية.","في 2، يستنتج TypeScript 5.5 وما بعده type predicate من <code dir=\"ltr\">(c) =&gt; c !== null</code>، لكن ليس من <code dir=\"ltr\">(c) =&gt; c</code>.","في 3، انسخ <code dir=\"ltr\">order.couponCode</code> إلى <code dir=\"ltr\">const</code> قبل فحصه، واستخدم ذلك الـ <code dir=\"ltr\">const</code> داخل الدالة السهمية."],"explanation":"<p><code dir=\"ltr\">!fee</code> صادقة مع <code dir=\"ltr\">0</code>، فسقط الشحن المجاني في فرع <code dir=\"ltr\">null</code>؛ والفحص بـ <code dir=\"ltr\">=== null</code> يفصل بين المعنيين. تُستنتج <code dir=\"ltr\">filter((c) =&gt; c !== null)</code> على أنها الـ predicate <code dir=\"ltr\">c is string</code>، فتضيق المصفوفة إلى <code dir=\"ltr\">string[]</code>، أما الـ callback القائم على الصدقية فليس predicate. وفي <code dir=\"ltr\">couponBanner</code>، لا ينجو تضييق الخاصية القابلة للتغيير <code dir=\"ltr\">order.couponCode</code> إلى داخل الـ callback، لكن الـ <code dir=\"ltr\">const</code> لا يتغيّر أبدًا، فينجو نوعه المضيَّق.</p>\n","checkLabels":{"types":"الملف يجتاز فحص الأنواع بلا أي خطأ","free-shipping":"<code dir=\"ltr\">shippingLabel</code> تُعيد ‘Free’ مع 0، و’Calculated at checkout’ مع null، و’$4.99’ مع 499","used-coupons":"<code dir=\"ltr\">usedCoupons</code> تُبقي الرموز غير الفارغة فقط، بالترتيب نفسه","banner":"<code dir=\"ltr\">couponBanner</code> تحوّل الرمز إلى أحرف كبيرة وتُعيد [] حين لا يوجد كوبون"}},"words":680,"readMinutes":4}