{"id":"l-at-5-3","courseId":"advanced-typescript","lang":"ar","kind":"lesson","summary":"صِف كود JavaScript لم تكتبه بملفات التصريحات، وحدّد أنواع المتغيّرات العامة ومتغيّرات البيئة، ووسّع الـ interfaces والوحدات الموجودة بدمج التصريحات وتوسيع الوحدات.","html":"<p>ليس كل ما يلمسه كود TypeScript لديك مكتوبًا بـ TypeScript. نظام الدفع في Cartwheel يحمّل مقتطف تحليلات بوسم <code dir=\"ltr\">&lt;script&gt;</code>، ويستخدم حاسبة شحن داخلية قديمة مكتوبة بـ JavaScript، ويقرأ متغيّرات البيئة عبر Vite، ويعمل بـ lib متأخّرة سنة عن المتصفّحات التي يعمل عليها. في كلٍّ من هذه الحالات، تحتاج إلى إخبار الـ compiler عن كود لا يستطيع رؤيته. وهذا ما وُجدت له ملفات التصريحات و<code dir=\"ltr\">declare</code>.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">declare: إنه موجود، ثق بي</h2>\n<p><code dir=\"ltr\">declare</code> تصف شيئًا موجودًا وقت التشغيل لكنه معرَّف في مكان آخر. لا تنتج أي JavaScript. والملف المنتهي بـ <code dir=\"ltr\">.d.ts</code> لا يحتوي إلا مثل هذه التصريحات:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">types/analytics.d.ts</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\"> AnalyticsEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'checkout_started'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'order_placed'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'payment_failed'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> cartwheelAnalytics</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:#8250DF;--shiki-dark:#D2A8FF\">  track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> AnalyticsEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">props</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:#0550AE;--shiki-dark:#79C0FF\">string</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:#0550AE;--shiki-dark:#79C0FF\"> void</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\">import</code> ولا <code dir=\"ltr\">export</code>، فهو <strong>script عام</strong>: كل ما فيه مرئي في كل ملف من المشروع. والآن صارت <code dir=\"ltr\">cartwheelAnalytics.track('order_plaecd')</code> خطأ ترجمة، وهذا ما لم تنجح فيه وثائق المقتطف أبدًا.</p>\n<p>التصريح وعدٌ غير مفحوص، تمامًا مثل تحويلات <code dir=\"ltr\">as</code> في الأقسام السابقة. إن اختلف الـ API الحقيقي للمقتطف، سيثق TypeScript بنسختك بسرور. أبقِ التصريحات صغيرة، ولا تكتب إلا الأجزاء التي تستخدمها، وضع تعليقًا بإصدار المكتبة الذي تحقّقت منها مقابله.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">تحديد أنواع حزمة JavaScript</h2>\n<p>قبل كتابة أي تصريحات لحزمة، تحقّق من المكانين اللذين تأتي منهما الأنواع عادة. حزم كثيرة تأتي بأنواعها الخاصة (<code dir=\"ltr\">&quot;types&quot;</code> في <code dir=\"ltr\">package.json</code> الخاص بها، أو ملفات <code dir=\"ltr\">.d.ts</code> بجوار JavaScript). ولغيرها، يصون المجتمع حزم <code dir=\"ltr\">@types/</code> على DefinitelyTyped: <code dir=\"ltr\">npm install -D @types/legacy-shipping-calc</code> إن وُجدت.</p>\n<p>ولحزمة داخلية ليس لها أيٌّ منهما، أعلن الوحدة بنفسك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">types/legacy-shipping-calc.d.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> module</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'legacy-shipping-calc'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Quote</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">carrier</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#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:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">days</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> quote</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">country</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\">weightGrams</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Quote</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\">declare module 'legacy-shipping-calc';</code> دون جسم تعمل أيضًا، وتجعل كل import من نوع <code dir=\"ltr\">any</code>. اعتبرها مخرج طوارئ مؤقتًا أثناء الترحيل، لا حلًا.</p>\n<p>أبقِ التصريحات المكتوبة يدويًا معًا في مجلد واحد، مثل <code dir=\"ltr\">types/</code>، وتأكّد أن <code dir=\"ltr\">include</code> في tsconfig يشمله؛ فملف <code dir=\"ltr\">.d.ts</code> لا يراه الـ compiler أبدًا لا يفعل شيئًا، ورسالة الخطأ الناتجة “Could not find a declaration file for module” مربكة في المرة الأولى. ولاحظ أيضًا أن هذا الملف عام، مثل ملف التحليلات: كتلة <code dir=\"ltr\">declare module 'name'</code> باسم نصي هي طريقتك لوصف حزمة من ملف script.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">دمج التصريحات</h2>\n<p>بعض التصريحات التي لها الاسم نفسه تتّحد بدل أن تتصادم. أكثر ما ستستخدمه: <strong>الـ interfaces تندمج</strong>.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutConfig</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutConfig</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">giftWrap</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</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\"> config</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutConfig</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { currency: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'EGP'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, giftWrap: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// both required</span></span></code></pre></div>\n<p>الأسماء المستعارة للأنواع لا تندمج أبدًا؛ تصريحان <code dir=\"ltr\">type CheckoutConfig</code> يعطيان خطأ “Duplicate identifier”. هذا هو الفرق العملي الرئيسي بين <code dir=\"ltr\">type</code> و<code dir=\"ltr\">interface</code>، ولهذا تكشف المكتبات التي تتوقّع أن تُوسَّع عن interfaces.</p>\n<p>والدمج هو طريقتك لسدّ الفجوات في المكتبة القياسية. بيئة التجربة في هذه الدورة تستخدم الـ lib الخاصة بـ ES2022، فـ <code dir=\"ltr\">Array.prototype.findLast</code> (من ES2023) غائبة عن أنواعها رغم أن كل متصفّح حالي يملكها. والدمج في الـ interface العام <code dir=\"ltr\">Array&lt;T&gt;</code> يضيفها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Array</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  findLast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">predicate</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">index</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\">array</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#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:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</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\">lib</code> أو <code dir=\"ltr\">target</code>. أما الدمج فللحالات التي لا تستطيع فيها الإعدادات التعبير عمّا تملكه بيئة التشغيل فعلًا، مثل polyfill تضمّنه بنفسك.</p>\n<p>وهو أيضًا الطريقة التي يحدّد بها Vite أنواع متغيّرات البيئة لديك. أنواع العميل فيه تعلن interface عامًا باسم <code dir=\"ltr\">ImportMetaEnv</code>، ويندمج مشروعك فيه:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">src/vite-env.d.ts</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:#6E7781;--shiki-dark:#8B949E\">/// &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">reference</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> types</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"vite/client\"</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\"> /></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ImportMetaEnv</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  readonly</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> VITE_API_URL</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\">  readonly</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> VITE_PAYMENTS_KEY</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<h2 id=\"sec-4\" tabindex=\"-1\">توسيع الوحدات</h2>\n<p>للإضافة إلى نوع يعيش <em>داخل</em> وحدة، افتح الوحدة بـ <code dir=\"ltr\">declare module</code> من ملف هو نفسه وحدة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">src/theme.d.ts</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\">import</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cartwheel-ui'</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\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> module</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cartwheel-ui'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Theme</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    brand</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">primary</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\">onPrimary</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:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>صار للـ interface المسمّى <code dir=\"ltr\">Theme</code> داخل <code dir=\"ltr\">cartwheel-ui</code> حقل <code dir=\"ltr\">brand</code> في كل مكان يُستخدم فيه. وللإضافة إلى النطاق العام من ملف وحدة، استخدم <code dir=\"ltr\">declare global { interface Window { dataLayer: unknown[] } }</code>.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الإعلان حين كنت تقصد التوسيع</span></p>\n<p>كتلة <code dir=\"ltr\">declare module 'cartwheel-ui' { … }</code> نفسها تعني شيئين مختلفين حسب ملفها. في ملف فيه <code dir=\"ltr\">import</code> أو <code dir=\"ltr\">export</code> واحد على الأقل، <strong>توسّع</strong> الوحدة الموجودة. وفي ملف ليس فيه أيٌّ منهما، <strong>تعلن</strong> وحدة محيطة جديدة كليًا تحجب الحزمة الحقيقية، فتختفي كل الصادرات الأخرى من <code dir=\"ltr\">cartwheel-ui</code>. إن بدا أن توسيعًا قد محا مكتبة، فأضف <code dir=\"ltr\">export {}</code> أو سطر الـ import، وتحقّق مرة أخرى.</p>\n</aside>\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>نشر أنواعك الخاصة</span></p>\n<p>إن كنت تنشر حزمة، فدع TypeScript يولّد تصريحاتها بـ <code dir=\"ltr\">&quot;declaration&quot;: true</code>. ومع <code dir=\"ltr\">isolatedDeclarations</code> (منذ TypeScript 5.5)، يتطلّب الـ compiler أنواعًا صريحة على كل ما تصدّره، كي تستطيع الأدوات الأسرع توليد ملفات <code dir=\"ltr\">.d.ts</code> ملفًا بعد ملف دون تشغيل فاحص الأنواع. وهي عادة جيدة للـ API العام لأي مكتبة على أي حال.</p>\n</aside>\n<p>في التمرين ستعلن متغيّر التحليلات العام، وتضيف <code dir=\"ltr\">findLast</code> إلى الـ lib في بيئة التجربة، وتوسّع interface إعداد بالدمج. الدرس الأخير: نقل قاعدة كود JavaScript كاملة.</p>\n","toc":[{"id":"sec-1","text":"declare: إنه موجود، ثق بي","level":2},{"id":"sec-2","text":"تحديد أنواع حزمة JavaScript","level":2},{"id":"sec-3","text":"دمج التصريحات","level":2},{"id":"sec-4","text":"توسيع الوحدات","level":2}],"takeaways":["ملف <code dir=\"ltr\">.d.ts</code> لا يحتوي إلا أنواعًا؛ و<code dir=\"ltr\">declare</code> تصف شيئًا موجودًا وقت التشغيل لكنه معرَّف في مكان آخر.","تحقّق من وجود أنواع مدمجة أو حزمة <code dir=\"ltr\">@types/</code> قبل كتابة التصريحات، ولا تكتب إلا أجزاء المكتبة التي تستخدمها.","الـ interfaces التي لها الاسم نفسه في النطاق نفسه تندمج في واحد؛ أما الأسماء المستعارة للأنواع فلا تندمج أبدًا، ولهذا تكون أنواع المكتبات القابلة للتوسيع interfaces.","<code dir=\"ltr\">declare module 'pkg' { … }</code> في ملف فيه import أو export توسّع تلك الوحدة؛ وفي ملف ليس فيه أيٌّ منهما تعلن وحدة محيطة جديدة كليًا.","<code dir=\"ltr\">declare global { … }</code> داخل وحدة تضيف إلى النطاق العام، مثل خاصية على <code dir=\"ltr\">Window</code> أو حقل في <code dir=\"ltr\">ImportMetaEnv</code>."],"further":[{"title":"Declaration Files - Introduction","url":"https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html"},{"title":"Declaration Merging","url":"https://www.typescriptlang.org/docs/handbook/declaration-merging.html"},{"title":"Env Variables and Modes (Vite)","url":"https://vite.dev/guide/env-and-mode.html"}],"quiz":[{"q":"<p>تضيف <code dir=\"ltr\">import { quote } from 'legacy-shipping-calc'</code>، وهي حزمة JavaScript بلا أنواع. ماذا يجب أن تجرّب أولًا؟</p>","options":[{"text":"اكتب <code dir=\"ltr\">.d.ts</code> كاملًا يصف كل ما تصدّره الحزمة.","why":"كتابة التصريحات هي الملاذ الأخير، وحتى حينها لا تحتاج إلا الأجزاء التي تستخدمها."},{"text":"أضف <code dir=\"ltr\">declare module 'legacy-shipping-calc';</code> كي يصبح كل شيء <code dir=\"ltr\">any</code>.","why":"هذا يُسكت الخطأ بتحويل كل import إلى <code dir=\"ltr\">any</code>، فيُخفي بالضبط الأخطاء التي وُجدت الأنواع لالتقاطها."},{"text":"حوّل الـ import بـ <code dir=\"ltr\">as any</code> في كل موضع استدعاء.","why":"الـ import نفسه يفشل في فحص الأنواع أولًا، ونثر <code dir=\"ltr\">any</code> يزيد الأمور سوءًا لا تحسّنًا."},{"text":"تحقّق إن كانت الحزمة تأتي بأنواعها الخاصة أو إن كانت هناك حزمة <code dir=\"ltr\">@types/legacy-shipping-calc</code>.","why":"صحيح. معظم الحزم الشائعة محدَّدة الأنواع بطريقة أو بأخرى؛ لا تكتب التصريحات إلا حين لا يوجد أيٌّ منهما."}],"answer":3},{"q":"<p>ما هو <code dir=\"ltr\">keyof CheckoutConfig</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\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutConfig</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutConfig</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">giftWrap</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">'currency' | 'giftWrap'</code>","why":"صحيح. الـ interfaces التي لها الاسم نفسه في النطاق نفسه تدمج أعضاءها."},{"text":"<code dir=\"ltr\">'giftWrap'</code>، لأن التصريح الثاني يستبدل الأول","why":"الـ interfaces لا يستبدل بعضها بعضًا أبدًا؛ إنها تندمج. أما الأسماء المستعارة المكرّرة للأنواع فهي خطأ."},{"text":"خطأ، “Duplicate identifier ‘CheckoutConfig’”.","why":"هذا الخطأ هو ما ستحصل عليه مع اسمين مستعارين بـ <code dir=\"ltr\">type</code>. الـ interfaces مصمَّمة لتندمج."}],"answer":0},{"q":"<p>ملف <code dir=\"ltr\">vite-env.d.ts</code> لا يحتوي إلا <code dir=\"ltr\">interface ImportMetaEnv { readonly VITE_API_URL: string }</code> (مع توجيه المرجع الخاص بـ Vite). ماذا يفعل؟</p>","options":[{"text":"يستبدل <code dir=\"ltr\">ImportMetaEnv</code> الخاص بـ Vite، مُزيلًا الحقلين المدمجين <code dir=\"ltr\">MODE</code> و<code dir=\"ltr\">DEV</code>.","why":"الـ interfaces تندمج، فتبقى حقول Vite وتُضاف حقولك."},{"text":"يضيف <code dir=\"ltr\">VITE_API_URL</code> إلى <code dir=\"ltr\">ImportMetaEnv</code> العام الموجود، فيكون <code dir=\"ltr\">import.meta.env.VITE_API_URL</code> من نوع <code dir=\"ltr\">string</code>.","why":"صحيح. الملف ليس فيه imports أو exports، فهو script عام، ويندمج الـ interface فيه مع الـ interface العام الخاص بـ Vite."},{"text":"يحدّد قيمة المتغيّر وقت البناء.","why":"التصريحات لا تنتج قيمًا أبدًا؛ القيمة ما زالت تأتي من ملف <code dir=\"ltr\">.env</code> الخاص بك."}],"answer":1},{"q":"<p>الملف <code dir=\"ltr\">types/ui.d.ts</code> لا يحتوي إلا <code dir=\"ltr\">declare module 'cartwheel-ui' { interface Theme { brand: string } }</code>. بعدها، صار كل import من <code dir=\"ltr\">cartwheel-ui</code> يفتقد بقية ما تصدّره الحزمة. لماذا؟</p>","options":[{"text":"الـ interfaces لا يمكن إعلانها داخل <code dir=\"ltr\">declare module</code>.","why":"يمكن ذلك؛ توسيع الوحدات في معظمه interfaces."},{"text":"الملف يحتاج إلى توجيه <code dir=\"ltr\">/// &lt;reference&gt;</code>.","why":"توجيه المرجع لا يغيّر ما إذا كانت الكتلة توسّع وحدة أم تعلن وحدة."},{"text":"دون أي import أو export يكون الملف script، فتعلن الكتلة وحدة محيطة جديدة كليًا تحجب الحقيقية؛ أضف <code dir=\"ltr\">export {}</code> أو import.","why":"صحيح. في ملف وحدة، <code dir=\"ltr\">declare module 'cartwheel-ui'</code> توسّع؛ وفي ملف script، تعلن من الصفر."}],"answer":2}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"// ---- Vendor snippet (do not edit) ----\n// In the real app this arrives in a <script> tag; here we simulate it.\nconst vendorQueue: string[] = [];\n(globalThis as Record<string, unknown>).cartwheelAnalytics = {\n  sent: vendorQueue,\n  track(event: string, props: Record<string, string | number> = {}) {\n    vendorQueue.push(`${event} ${JSON.stringify(props)}`);\n  },\n};\n// ---------------------------------------\n\ntype AnalyticsEvent = 'checkout_started' | 'order_placed' | 'payment_failed';\n\n// \"Library\" config interface (pretend it lives in a package you cannot edit).\ninterface CheckoutConfig {\n  currency: string;\n}\n\n// Your declarations go here.\n\ntype Order = { id: number; status: 'processing' | 'shipped' | 'delivered'; totalCents: number };\n\nconst orders: Order[] = [\n  { id: 1041, status: 'shipped', totalCents: 4500 },\n  { id: 1042, status: 'shipped', totalCents: 18900 },\n  { id: 1043, status: 'processing', totalCents: 1200 },\n];\n\nfunction latestShipped(list: Order[]): Order | undefined {\n  return list.findLast((o) => o.status === 'shipped');\n}\n\nfunction placeOrder(order: Order): void {\n  cartwheelAnalytics.track('order_placed', { orderId: order.id, totalCents: order.totalCents });\n}\n\nconst config: CheckoutConfig = { currency: 'EGP', giftWrap: true };\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<keyof CheckoutConfig, 'currency' | 'giftWrap'>>;\ntype T2 = Expect<Equal<ReturnType<typeof latestShipped>, Order | undefined>>;\n\nfunction typeTests() {\n  // @ts-expect-error: not a known analytics event\n  cartwheelAnalytics.track('ordr_placed');\n  // @ts-expect-error: property values must be strings or numbers\n  cartwheelAnalytics.track('checkout_started', { express: true });\n}\n\nplaceOrder(latestShipped(orders)!);\nconsole.log(latestShipped(orders)?.id, cartwheelAnalytics.sent, config);\n"},"solution":{"ts":"// ---- Vendor snippet (do not edit) ----\n// In the real app this arrives in a <script> tag; here we simulate it.\nconst vendorQueue: string[] = [];\n(globalThis as Record<string, unknown>).cartwheelAnalytics = {\n  sent: vendorQueue,\n  track(event: string, props: Record<string, string | number> = {}) {\n    vendorQueue.push(`${event} ${JSON.stringify(props)}`);\n  },\n};\n// ---------------------------------------\n\ntype AnalyticsEvent = 'checkout_started' | 'order_placed' | 'payment_failed';\n\n// \"Library\" config interface (pretend it lives in a package you cannot edit).\ninterface CheckoutConfig {\n  currency: string;\n}\n\n// The analytics snippet, as far as we use it (checked against snippet v3).\ndeclare var cartwheelAnalytics: {\n  readonly sent: readonly string[];\n  track(event: AnalyticsEvent, props?: Record<string, string | number>): void;\n};\n\n// ES2023 method, present in every current browser but missing from the ES2022 lib.\ninterface Array<T> {\n  findLast(predicate: (value: T, index: number, array: T[]) => unknown): T | undefined;\n}\n\n// Merges with the library's CheckoutConfig.\ninterface CheckoutConfig {\n  giftWrap: boolean;\n}\n\ntype Order = { id: number; status: 'processing' | 'shipped' | 'delivered'; totalCents: number };\n\nconst orders: Order[] = [\n  { id: 1041, status: 'shipped', totalCents: 4500 },\n  { id: 1042, status: 'shipped', totalCents: 18900 },\n  { id: 1043, status: 'processing', totalCents: 1200 },\n];\n\nfunction latestShipped(list: Order[]): Order | undefined {\n  return list.findLast((o) => o.status === 'shipped');\n}\n\nfunction placeOrder(order: Order): void {\n  cartwheelAnalytics.track('order_placed', { orderId: order.id, totalCents: order.totalCents });\n}\n\nconst config: CheckoutConfig = { currency: 'EGP', giftWrap: true };\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<keyof CheckoutConfig, 'currency' | 'giftWrap'>>;\ntype T2 = Expect<Equal<ReturnType<typeof latestShipped>, Order | undefined>>;\n\nfunction typeTests() {\n  // @ts-expect-error: not a known analytics event\n  cartwheelAnalytics.track('ordr_placed');\n  // @ts-expect-error: property values must be strings or numbers\n  cartwheelAnalytics.track('checkout_started', { express: true });\n}\n\nplaceOrder(latestShipped(orders)!);\nconsole.log(latestShipped(orders)?.id, cartwheelAnalytics.sent, config);\n"},"checks":[{"id":"types","typecheck":true},{"id":"latest","test":"return latestShipped(orders).id === 1042 && latestShipped([]) === undefined;"},{"id":"tracks","test":"return cartwheelAnalytics.sent.some((s) => s.startsWith('order_placed') && s.includes('1042'));"}],"title":"أعلن ما لا يستطيع الـ compiler رؤيته","prompt":"<p>كود التطبيق أدناه يعمل وقت التشغيل أصلًا، لكنه لا يجتاز فحص الأنواع. دون تعديل كود التطبيق أو الاختبارات، أضف تصريحات في الموضع المحدّد:</p>\n<ol>\n<li>أعلن المتغيّر العام <code dir=\"ltr\">cartwheelAnalytics</code> من مقتطف المزوّد: method باسم <code dir=\"ltr\">track(event, props?)</code> حيث <code dir=\"ltr\">event</code> من نوع <code dir=\"ltr\">AnalyticsEvent</code> و<code dir=\"ltr\">props</code> تربط نصوصًا بنصوص أو أرقام، إضافة إلى قائمة <code dir=\"ltr\">sent</code> للقراءة فقط من النصوص.</li>\n<li>أضف <code dir=\"ltr\">findLast</code> إلى المصفوفات (موجودة في المتصفّح لكن ليس في الـ lib الخاصة بـ ES2022 في بيئة التجربة)، تُعيد نوع العنصر أو <code dir=\"ltr\">undefined</code>.</li>\n<li>وسّع <code dir=\"ltr\">CheckoutConfig</code> بحقل إلزامي <code dir=\"ltr\">giftWrap: boolean</code>، دون تعديل الـ interface الأصلي.</li>\n</ol>\n","hints":["المتغيّر العام الموجود وقت التشغيل يُوصف بـ <code dir=\"ltr\">declare var name: { … };</code> (هذا الملف script، فهو عام).","يمكن توسيع الـ interfaces العامة في الـ lib بإعلان interface بالاسم نفسه: <code dir=\"ltr\">interface Array&lt;T&gt; { findLast(…): T | undefined; }</code>.","الـ interfaces تندمج: اكتب <code dir=\"ltr\">interface CheckoutConfig { giftWrap: boolean; }</code> ثانيًا."],"explanation":"<p><code dir=\"ltr\">declare var</code> تخبر TypeScript أن <code dir=\"ltr\">cartwheelAnalytics</code> موجود وقت التشغيل دون توليد أي كود، والمعامل الدقيق <code dir=\"ltr\">AnalyticsEvent</code> يحوّل الأخطاء الإملائية إلى أخطاء ترجمة لن يبلّغ عنها المقتطف نفسه أبدًا. ولأن هذا الملف script، يندمج <code dir=\"ltr\">interface Array&lt;T&gt;</code> الثاني مع <code dir=\"ltr\">Array</code> العام في الـ lib، فيسدّ الفجوة بين الـ lib الخاصة بـ ES2022 والمتصفّح. ويندمج تصريح <code dir=\"ltr\">CheckoutConfig</code> الثاني مع الأول، وهذا بالضبط كيف يوسّع توسيعُ الوحدات الـ interfaces الخاصة بمكتبة. والثلاثة كلها وعود غير مفحوصة بشأن بيئة التشغيل، فيجب أن تبقى في حدّها الأدنى وأن تذكر ما تحقّقت منه مقابله.</p>\n","checkLabels":{"types":"الملف يجتاز فحص الأنواع بلا أي خطأ، بما فيها اختبارات الأنواع","latest":"<code dir=\"ltr\">latestShipped</code> تُعيد آخر طلب مشحون","tracks":"<code dir=\"ltr\">placeOrder</code> ترسل حدث <code dir=\"ltr\">order_placed</code>"}},"words":648,"readMinutes":4}