{"id":"l-at-1-5","courseId":"advanced-typescript","lang":"ar","kind":"lesson","summary":"امنع القيم الحرفية من الاتساع إلى string، واشتقّ أنواع union من مصفوفات وقت التشغيل باستخدام as const، وتحقّق من كائنات الإعداد بـ satisfies دون أن تفقد أنواعها الدقيقة.","html":"<p>إليك بلاغ خطأ قدّمه كل فريق TypeScript مرة على الأقل: “أمرّر النص الصحيح بالضبط ويقول لي <code dir=\"ltr\">Type 'string' is not assignable to type 'OrderStatus'</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'returned'</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\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type '{ id: number; status: string; }' is not assignable</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// to parameter of type 'Order'.</span></span></code></pre></div>\n<p>النص <em>هو</em> <code dir=\"ltr\">'processing'</code> فعلًا. لكن TypeScript لم يستنتج له <code dir=\"ltr\">'processing'</code>، بل استنتج <code dir=\"ltr\">string</code>. هذا هو <strong>الاتساع (widening)</strong>، وفهمه يشرح ثلاث أدوات ستستخدمها كل أسبوع: الـ literal types (الأنواع الحرفية)، و<code dir=\"ltr\">as const</code>، و<code dir=\"ltr\">satisfies</code>.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">لماذا تتّسع القيم الحرفية</h2>\n<p>يستنتج TypeScript النوع الذي يُرجَّح أن تحمله القيمة <em>طوال حياتها</em>، لا القيمة التي تبدأ بها فقط. الـ <code dir=\"ltr\">const</code> لا يمكن أن يتغيّر أبدًا، فيحتفظ بالقيمة الحرفية. أما <code dir=\"ltr\">let</code> أو خاصية الكائن فيمكن إعادة الإسناد إليهما، فيوسّعهما 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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> a</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;           </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 'processing'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> b </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;             </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// string</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> c</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// { status: string }</span></span></code></pre></div>\n<p>الأخيرة هي المفاجأة. <code dir=\"ltr\">const c</code> تمنعك من إعادة الإسناد إلى <code dir=\"ltr\">c</code>، لكن <code dir=\"ltr\">c.status = 'anything'</code> ما زالت JavaScript مشروعة، فتتّسع الخاصية إلى <code dir=\"ltr\">string</code>.</p>\n<p>هناك ثلاثة إصلاحات جيدة، مرتّبة حسب الأفضلية. اكتب القيمة الحرفية حيث يكون النوع معروفًا أصلًا (<code dir=\"ltr\">save({ id: 1001, status: 'processing' })</code>)، فتُفحص مقابل <code dir=\"ltr\">Order</code> مباشرة. أو علّق المتغيّر بنوعه (<code dir=\"ltr\">const draft: Order = …</code>) حين تبنيه مسبقًا. أو استخدم <code dir=\"ltr\">as const</code>، وهي موضوعنا التالي.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">as const: جمّد القيم الحرفية</h2>\n<p><code dir=\"ltr\">as const</code> تطلب من TypeScript أن يستنتج أضيق نوع ممكن: كل قيمة حرفية تبقى حرفية، وكل مصفوفة تصبح tuple للقراءة فقط، وكل خاصية تصبح <code dir=\"ltr\">readonly</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\"> COUNTRIES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'EG'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'AE'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'SA'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'JO'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'GB'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'DE'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'US'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'CA'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// readonly ['EG', 'AE', 'SA', 'JO', 'GB', 'DE', 'US', 'CA']</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Country</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNTRIES</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[</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:#6E7781;--shiki-dark:#8B949E\">// 'EG' | 'AE' | 'SA' | 'JO' | 'GB' | 'DE' | 'US' | 'CA'</span></span></code></pre></div>\n<p>ذلك السطر الثاني من أنفع الأنماط في TypeScript اليومية. <code dir=\"ltr\">typeof COUNTRIES</code> تحوّل القيمة إلى نوعها، و<code dir=\"ltr\">[number]</code> تسأل “ما النوع الذي أحصل عليه حين أفهرس هذا بأي رقم؟” (ستتعرّف على هذين العاملين (operators) كما ينبغي في <a href=\"/ar/lesson/advanced-typescript/l-at-3-1\">keyof و typeof وأنواع الوصول بالفهرس</a>). والنتيجة union <strong>مشتقّ من مصفوفة وقت التشغيل</strong>. تحصل على مصدر حقيقة واحد: المصفوفة موجودة وقت التشغيل للقوائم المنسدلة والتحقّق، والنوع يتبعها تلقائيًا.</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\"> isCountry</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> is</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Country</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\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">COUNTRIES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> readonly</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">includes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(value);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>التحويل الموسِّع داخل <code dir=\"ltr\">isCountry</code> ضروري لأن <code dir=\"ltr\">includes</code> على tuple من القيم الحرفية لا تقبل إلا تلك القيم، وهذا يُفسد الغاية من فحص نص اعتباطي.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>فضّل الـ unions الحرفية على الـ enums</span></p>\n<p><code dir=\"ltr\">enum OrderStatus { Processing = 'processing', … }</code> يعطيك union مشابهًا، لكن الـ enums تولّد كودًا وقت التشغيل، ولا تتوافق مع القيم النصية العادية القادمة من JSON دون تحويل، ولا يدعمها تجريد الأنواع (type stripping): يرفضها دعم TypeScript المدمج في Node، وكذلك الخيار <code dir=\"ltr\">erasableSyntaxOnly</code>. مصفوفة أو كائن <code dir=\"ltr\">as const</code> مع union مشتقّ يؤدّيان المهمة نفسها بـ JavaScript عادية.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">التعليق النوعي مقابل satisfies</h2>\n<p>كوبونات Cartwheel موجودة في كائن إعداد. الطريقة البديهية لتحديد نوعه هي التعليق النوعي:</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\"> Coupon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">kind</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">percentOff</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\">kind</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'free-shipping'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> coupons</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</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\">Coupon</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  WELCOME10: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, percentOff: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  FREESHIP: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'free-shipping'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">};</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupons.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">WELCOME10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.percentOff; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Property 'percentOff' does not exist on type 'Coupon'.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupons.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">SPRNG15</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;              </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// compiles, undefined at runtime</span></span></code></pre></div>\n<p>التعليق النوعي <em>يستبدل</em> ما كان TypeScript يعرفه. كتبت <code dir=\"ltr\">WELCOME10</code> ككوبون نسبة مئوية، لكن نوعه صار مجرّد <code dir=\"ltr\">Coupon</code>، والكائن يقبل أي مفتاح على الإطلاق، بما فيها الأخطاء الإملائية.</p>\n<p>أما <code dir=\"ltr\">satisfies</code> فتتحقّق من القيمة مقابل نوع <strong>دون أن تستبدل النوع المستنتَج</strong>:</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\">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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  WELCOME10: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, percentOff: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  FREESHIP: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'free-shipping'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">} </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">satisfies</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</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\">Coupon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupons.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">WELCOME10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.percentOff; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// number</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupons.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">SPRNG15</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;              </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Property 'SPRNG15' does not exist on type …</span></span></code></pre></div>\n<p>ما زلت تحصل على التحقّق: <code dir=\"ltr\">percentof</code> المكتوبة خطأً أو <code dir=\"ltr\">kind: 'bogo'</code> تُعلَّم مباشرة في الكائن الحرفي. وتحتفظ بالمفاتيح والأشكال الدقيقة. كما أن <code dir=\"ltr\">satisfies</code> تحدّد نوع القيم الحرفية من السياق، فيبقى <code dir=\"ltr\">kind</code> بقيمة <code dir=\"ltr\">'percent'</code> بدل أن يتّسع إلى <code dir=\"ltr\">string</code>، دون الحاجة إلى <code dir=\"ltr\">as const</code>.</p>\n<p>استخدم التركيبة <code dir=\"ltr\">as const satisfies T</code> حين تريد الأمرين: قيمًا حرفية للقراءة فقط بعمق، وفحصًا مقابل شكل. تُقرأ من اليسار إلى اليمين هكذا: “جمّد هذا، ثم تأكّد أنه مناسب”.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>استخدام as لإخفاء خطأ</span></p>\n<p><code dir=\"ltr\">const config = { retries: 3 } as CheckoutConfig</code> يمرّ في الترجمة حتى حين يتطلّب <code dir=\"ltr\">CheckoutConfig</code> الخاصية <code dir=\"ltr\">timeoutMs</code>، لأن التأكيد لا يفحص إلا تداخل النوعين. إنه أنت تنقض حكم الـ compiler. احتفظ بـ <code dir=\"ltr\">as</code> للحالة النادرة التي تعرف فيها أكثر من TypeScript (واترك تعليقًا يشرح السبب). وحين تريد فحصًا، استخدم تعليقًا نوعيًا أو <code dir=\"ltr\">satisfies</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">الاختيار بينها</h2>\n<p>قاعدة افتراضية بسيطة: <strong>علّق</strong> معاملات الدوال وأنواع الإرجاع والمتغيّرات التي يجب أن يكون نوعها هو النوع العام (<code dir=\"ltr\">let current: Order</code>). استخدم <strong><code dir=\"ltr\">satisfies</code></strong> لكائنات الإعداد وجداول البحث وكل ما تهمّ فيه المفاتيح والقيم المحدّدة لاحقًا. واستخدم <strong><code dir=\"ltr\">as const</code></strong> للقوائم الثابتة التي تريدها union. الثلاثة كلها وقت الترجمة فقط؛ لا يغيّر أيٌّ منها بايتًا واحدًا من JavaScript الناتجة.</p>\n<p>في التمرين ستشتقّ <code dir=\"ltr\">Country</code> من قائمة دول Cartwheel، وتحدّد نوع جدول العملات بحيث يرفض الدول المجهولة ويتذكّر العملات بدقة. بهذا تكتمل أدوات التصميم في هذا القسم؛ وبعده تنتقل إلى الـ generics، بدءًا بالعلاقات التي تعبّر عنها.</p>\n","toc":[{"id":"sec-1","text":"لماذا تتّسع القيم الحرفية","level":2},{"id":"sec-2","text":"as const: جمّد القيم الحرفية","level":2},{"id":"sec-3","text":"التعليق النوعي مقابل satisfies","level":2},{"id":"sec-4","text":"الاختيار بينها","level":2}],"takeaways":["متغيّرات <code dir=\"ltr\">let</code> وخصائص الكائنات توسّع القيم الحرفية إلى <code dir=\"ltr\">string</code> أو <code dir=\"ltr\">number</code>؛ أما متغيّرات <code dir=\"ltr\">const</code> فتحتفظ بالقيمة الحرفية.","<code dir=\"ltr\">as const</code> تجعل القيمة للقراءة فقط بعمق وتحتفظ بكل قيمة حرفية، ما يتيح لك اشتقاق نوع union من مصفوفة وقت التشغيل.","التعليق النوعي (annotation) يستبدل النوع المستنتَج بالنوع المكتوب؛ أما <code dir=\"ltr\">satisfies</code> فتتحقّق مقابل نوع وتحتفظ بالنوع المستنتَج.","تأكيدات <code dir=\"ltr\">as</code> تتخطّى فحوصًا مثل الخصائص الناقصة، لذا استخدم <code dir=\"ltr\">satisfies</code> حين تريد تحقّقًا لا ثقة عمياء."],"further":[{"title":"The satisfies operator (TypeScript 4.9 release notes)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html"},{"title":"Literal types","url":"https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types"},{"title":"const assertions (TypeScript 3.4 release notes)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html"}],"quiz":[{"q":"<p>لماذا تفشل <code dir=\"ltr\">save(draft)</code> حين يكون <code dir=\"ltr\">Order['status']</code> هو <code dir=\"ltr\">'processing' | 'shipped'</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft);</span></span></code></pre></div>\n","options":[{"text":"لأن <code dir=\"ltr\">const</code> تجعل الكائن كله للقراءة فقط، و<code dir=\"ltr\">save</code> تتوقّع كائنًا قابلًا للتعديل.","why":"<code dir=\"ltr\">const</code> تمنع فقط إعادة الإسناد إلى <code dir=\"ltr\">draft</code>. أما خصائصه فتبقى قابلة للتعديل، ولهذا بالضبط تتّسع."},{"text":"لأن <code dir=\"ltr\">draft.status</code> يُستنتج على أنه <code dir=\"ltr\">string</code>؛ خصائص الكائنات تتّسع لأنه قد يُعاد الإسناد إليها لاحقًا.","why":"صحيح. اكتب القيمة الحرفية حيث يكون النوع معروفًا (<code dir=\"ltr\">save({ … })</code>)، أو علّق <code dir=\"ltr\">draft</code> بالنوع <code dir=\"ltr\">Order</code>، أو استخدم <code dir=\"ltr\">as const</code>."},{"text":"لأن <code dir=\"ltr\">draft</code> ليس فيه <code dir=\"ltr\">customerId</code>.","why":"الـ <code dir=\"ltr\">Order</code> في السؤال لا يحتاج إلا <code dir=\"ltr\">id</code> و<code dir=\"ltr\">status</code>. الفشل سببه <code dir=\"ltr\">status</code> المتّسع."}],"answer":1},{"q":"<p>ما هو <code dir=\"ltr\">Country</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\"> COUNTRIES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'EG'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'AE'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'GB'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</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\"> Country</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNTRIES</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">string</code>","why":"هذه ستكون النتيجة دون <code dir=\"ltr\">as const</code>، حيث تُستنتج المصفوفة على أنها <code dir=\"ltr\">string[]</code>."},{"text":"<code dir=\"ltr\">readonly ['EG', 'AE', 'GB']</code>","why":"هذا هو <code dir=\"ltr\">typeof COUNTRIES</code>. وفهرسته بـ <code dir=\"ltr\">[number]</code> تعطي نوع عناصره."},{"text":"<code dir=\"ltr\">'EG' | 'AE' | 'GB'</code>","why":"صحيح. <code dir=\"ltr\">as const</code> تحتفظ بالقيم الحرفية، و<code dir=\"ltr\">[number]</code> تطلب نوع أي عنصر في الـ tuple."}],"answer":2},{"q":"<p>عُرّف <code dir=\"ltr\">coupons</code> مع <code dir=\"ltr\">satisfies Record&lt;string, Coupon&gt;</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\"> Coupon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">kind</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">percentOff</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\">kind</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'free-shipping'</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\"> coupons</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  WELCOME10: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, percentOff: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  FREESHIP: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'free-shipping'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">} </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">satisfies</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</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\">Coupon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">coupons.WELCOME10.percentOff</code>","why":"<code dir=\"ltr\">satisfies</code> تحتفظ بالنوع المستنتَج، فيعرف TypeScript أن <code dir=\"ltr\">WELCOME10</code> هو شكل النسبة المئوية وأن <code dir=\"ltr\">percentOff</code> موجودة."},{"text":"<code dir=\"ltr\">coupons.FREESHIP.kind === 'free-shipping'</code>","why":"النوع المستنتَج لـ <code dir=\"ltr\">kind</code> هو القيمة الحرفية <code dir=\"ltr\">'free-shipping'</code>، فهذه المقارنة صالحة (وصادقة دائمًا)."},{"text":"<code dir=\"ltr\">coupons.SPRING15</code>","why":"صحيح. النوع المستنتَج لا يحتوي إلا المفتاحين اللذين كتبتهما، فقراءة مفتاح مجهول خطأ، بخلاف التعليق النوعي <code dir=\"ltr\">Record&lt;string, Coupon&gt;</code>."}],"answer":2},{"q":"<p>يكتب زميل <code dir=\"ltr\">const config = { retries: 3 } as CheckoutConfig;</code>، و<code dir=\"ltr\">CheckoutConfig</code> يتطلّب أيضًا <code dir=\"ltr\">timeoutMs</code>. ماذا يحدث؟</p>","options":[{"text":"يمرّ في الترجمة، لأن التأكيد لا يحتاج إلا أن يتداخل النوعان؛ ولا يُبلَّغ أبدًا عن <code dir=\"ltr\">timeoutMs</code> الناقصة.","why":"صحيح. <code dir=\"ltr\">as</code> تطلب من الـ compiler أن يثق بك. استخدم <code dir=\"ltr\">satisfies CheckoutConfig</code> أو تعليقًا نوعيًا لتحصل على خطأ الخاصية الناقصة."},{"text":"يفشل مع “Property ‘timeoutMs’ is missing”.","why":"هذا الخطأ يأتي من التعليقات النوعية ومن <code dir=\"ltr\">satisfies</code>. أما التأكيد فلا يرفض إلا الأنواع التي لا تتداخل إطلاقًا."},{"text":"يضيف <code dir=\"ltr\">timeoutMs: undefined</code> إلى الكائن وقت التشغيل.","why":"تأكيدات الأنواع تُمحى كليًا؛ لا تضيف قيمًا ولا تغيّرها أبدًا."}],"answer":0}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type Currency = 'EGP' | 'AED' | 'SAR' | 'JOD' | 'GBP' | 'EUR' | 'USD' | 'CAD';\ntype Coupon = { kind: 'percent'; percentOff: number } | { kind: 'free-shipping' };\n\nconst COUNTRIES = ['EG', 'AE', 'SA', 'JO', 'GB', 'DE', 'US', 'CA'];\ntype Country = string; // derive this from COUNTRIES\n\nconst currencyByCountry: Record<string, Currency> = {\n  EG: 'EGP',\n  AE: 'AED',\n  SA: 'SAR',\n  JO: 'JOD',\n  GB: 'GBP',\n  DE: 'EUR',\n  US: 'USD',\n  CA: 'CAD',\n};\n\nconst coupons: Record<string, Coupon> = {\n  WELCOME10: { kind: 'percent', percentOff: 10 },\n  SPRING15: { kind: 'percent', percentOff: 15 },\n  FREESHIP: { kind: 'free-shipping' },\n};\n\nfunction isCountry(value: string): value is Country {\n  return COUNTRIES.includes(value);\n}\n\nfunction currencyFor(country: Country): Currency {\n  return currencyByCountry[country];\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<Country, 'EG' | 'AE' | 'SA' | 'JO' | 'GB' | 'DE' | 'US' | 'CA'>>;\ntype T2 = Expect<Equal<(typeof currencyByCountry)['DE'], 'EUR'>>;\ntype T3 = Expect<Equal<keyof typeof coupons, 'WELCOME10' | 'SPRING15' | 'FREESHIP'>>;\ntype T4 = Expect<Equal<(typeof coupons)['SPRING15']['percentOff'], number>>;\n\n// @ts-expect-error: France is not a Cartwheel country\ncurrencyByCountry.FR;\n\n// @ts-expect-error: typo in the coupon code\ncoupons.SPRNG15;\n\nconsole.log(currencyFor('DE'), isCountry('EG'), isCountry('FR'));\n"},"solution":{"ts":"type Currency = 'EGP' | 'AED' | 'SAR' | 'JOD' | 'GBP' | 'EUR' | 'USD' | 'CAD';\ntype Coupon = { kind: 'percent'; percentOff: number } | { kind: 'free-shipping' };\n\nconst COUNTRIES = ['EG', 'AE', 'SA', 'JO', 'GB', 'DE', 'US', 'CA'] as const;\ntype Country = (typeof COUNTRIES)[number];\n\nconst currencyByCountry = {\n  EG: 'EGP',\n  AE: 'AED',\n  SA: 'SAR',\n  JO: 'JOD',\n  GB: 'GBP',\n  DE: 'EUR',\n  US: 'USD',\n  CA: 'CAD',\n} as const satisfies Record<Country, Currency>;\n\nconst coupons = {\n  WELCOME10: { kind: 'percent', percentOff: 10 },\n  SPRING15: { kind: 'percent', percentOff: 15 },\n  FREESHIP: { kind: 'free-shipping' },\n} satisfies Record<string, Coupon>;\n\nfunction isCountry(value: string): value is Country {\n  return (COUNTRIES as readonly string[]).includes(value);\n}\n\nfunction currencyFor(country: Country): Currency {\n  return currencyByCountry[country];\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<Country, 'EG' | 'AE' | 'SA' | 'JO' | 'GB' | 'DE' | 'US' | 'CA'>>;\ntype T2 = Expect<Equal<(typeof currencyByCountry)['DE'], 'EUR'>>;\ntype T3 = Expect<Equal<keyof typeof coupons, 'WELCOME10' | 'SPRING15' | 'FREESHIP'>>;\ntype T4 = Expect<Equal<(typeof coupons)['SPRING15']['percentOff'], number>>;\n\n// @ts-expect-error: France is not a Cartwheel country\ncurrencyByCountry.FR;\n\n// @ts-expect-error: typo in the coupon code\ncoupons.SPRNG15;\n\nconsole.log(currencyFor('DE'), isCountry('EG'), isCountry('FR'));\n"},"checks":[{"id":"types","typecheck":true},{"id":"is-country","test":"return isCountry('EG') === true && isCountry('CA') === true && isCountry('FR') === false;"},{"id":"currency","test":"return currencyFor('DE') === 'EUR' && currencyFor('JO') === 'JOD';"}],"title":"اشتقّ الدول، وافحص الجداول","prompt":"<p>اجعل جداول البحث في Cartwheel دقيقة دون أن تكرّر نفسك:</p>\n<ol>\n<li>اشتقّ <code dir=\"ltr\">Country</code> من المصفوفة <code dir=\"ltr\">COUNTRIES</code>، بحيث يكون الـ union <code dir=\"ltr\">'EG' | 'AE' | … | 'CA'</code>.</li>\n<li>حدّد نوع <code dir=\"ltr\">currencyByCountry</code> بحيث يجب أن يحتوي مدخلًا واحدًا لكل <code dir=\"ltr\">Country</code>، وبحيث يكون نوع <code dir=\"ltr\">currencyByCountry.DE</code> هو <code dir=\"ltr\">'EUR'</code> بالضبط.</li>\n<li>حدّد نوع <code dir=\"ltr\">coupons</code> بحيث تُفحص كل قيمة مقابل <code dir=\"ltr\">Coupon</code>، لكن لا يحتوي الكائن إلا المفاتيح الثلاثة التي كتبتها، ويُعرف أن <code dir=\"ltr\">SPRING15</code> كوبون نسبة مئوية.</li>\n</ol>\n<p>أبقِ <code dir=\"ltr\">isCountry</code> تعمل وقت التشغيل مع أي نص.</p>\n","hints":["<code dir=\"ltr\">as const</code> على المصفوفة تحتفظ بقيمها الحرفية؛ و<code dir=\"ltr\">(typeof COUNTRIES)[number]</code> تحوّلها إلى union.","استبدل التعليقات النوعية على الكائنين بـ <code dir=\"ltr\">satisfies</code>: <code dir=\"ltr\">{ … } satisfies Record&lt;Country, Currency&gt;</code>. ولأن <code dir=\"ltr\">Currency</code> هو union من قيم حرفية، تُبقي <code dir=\"ltr\">satisfies</code> وحدها <code dir=\"ltr\">'EUR'</code> حرفية؛ وإضافة <code dir=\"ltr\">as const</code> قبلها تجعل الجدول للقراءة فقط أيضًا.","حين تصير <code dir=\"ltr\">COUNTRIES</code> tuple للقراءة فقط، لا تقبل <code dir=\"ltr\">includes</code> إلا الدول. وسّعها من أجل الفحص: <code dir=\"ltr\">(COUNTRIES as readonly string[]).includes(value)</code>."],"explanation":"<p><code dir=\"ltr\">as const</code> تجمّد المصفوفة في tuple للقراءة فقط من القيم الحرفية، وفهرسة نوعها بـ <code dir=\"ltr\">[number]</code> تعطي الـ union من عناصرها: قائمة واحدة تقود فحص وقت التشغيل والنوع معًا. التعليقات النوعية على الجدولين استبدلت أنواعهما المستنتَجة بأنواع عامة (أي مفتاح نصي، وقيم <code dir=\"ltr\">Currency</code>، وأشكال <code dir=\"ltr\">Coupon</code>). أما <code dir=\"ltr\">satisfies</code> فتحتفظ بالفحص (دولة ناقصة، أو <code dir=\"ltr\">percentOff</code> مكتوبة خطأً، أو عملة مجهولة ستُعلَّم كلها) مع الحفاظ على المفاتيح والقيم الدقيقة. ولأن النوع السياقي <code dir=\"ltr\">Currency</code> هو union من قيم حرفية، تبقى <code dir=\"ltr\">'EUR'</code> حرفية بدل أن تتّسع إلى <code dir=\"ltr\">string</code>؛ أما <code dir=\"ltr\">as const satisfies …</code> فتجعل الجدول فوق ذلك للقراءة فقط.</p>\n","checkLabels":{"types":"الملف يجتاز فحص الأنواع بلا أي خطأ، بما فيها كل اختبارات الأنواع","is-country":"<code dir=\"ltr\">isCountry</code> تقبل دول Cartwheel وترفض غيرها وقت التشغيل","currency":"<code dir=\"ltr\">currencyFor</code> تُعيد العملة الصحيحة"}},"words":633,"readMinutes":4}