{"id":"l-at-2-4","courseId":"advanced-typescript","lang":"ar","kind":"lesson","summary":"صمّم عميل fetch تُستمدّ أنواعه من خريطة مسارات واحدة، بحيث يستنتج كل استدعاء أنواع مدخله واستجابته من اسم المسار، ولا يكتب المستدعون أبدًا وسيط نوع أو تحويلًا.","html":"<p>كل عميل API يبدأ بالطريقة نفسها: <code dir=\"ltr\">fetch(url).then((r) =&gt; r.json())</code>، التي تُعيد <code dir=\"ltr\">Promise&lt;any&gt;</code>، فتمتلئ قاعدة الكود ببطء بـ <code dir=\"ltr\">as Order</code> و<code dir=\"ltr\">as Order[]</code> في مواضع الاستدعاء. كل تحويل ادّعاءٌ صغير غير مُراجَع بشأن الخادم. هذا الدرس يجمع القسم كله لبناء عميل لا يكتب فيه المستدعون أي تحويل أو وسيط نوع، ويفشل فيه المسار الخاطئ أو الحقل الناقص أو المعرّف المكتوب بنوع خاطئ في الترجمة.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">مصدر حقيقة واحد: خريطة المسارات</h2>\n<p>ابدأ بكتابة كل endpoint مرة واحدة، كـ type:</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 style=\"color:#953800;--shiki-dark:#FFA657\">totalCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CancelledOrder</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:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cancelReason</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">items</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\">nextCursor</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</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:#0A3069;--shiki-dark:#A5D6FF\">  'GET /orders/:id'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#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\">output</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 style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'GET /orders'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#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 style=\"color:#953800;--shiki-dark:#FFA657\">cursor</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\">output</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> };</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'POST /orders/:id/cancel'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#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\">reason</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\">output</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CancelledOrder</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>كل مفتاح يسمّي endpoint، الطريقة والمسار معًا. وكل قيمة تقول ما يأخذه الـ endpoint وما يُعيده. لن يذكر أي شيء آخر في العميل <code dir=\"ltr\">Order</code> أو <code dir=\"ltr\">Page</code> مباشرة؛ كل ذلك سيُشتقّ من هذا الجدول. وإضافة endpoint تعني إضافة سطر واحد هنا.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">اسم المسار هو موقع الاستنتاج الوحيد</h2>\n<p>الآن التوقيع. تريد أن يكتب المستدعي المسار والمدخل، ويحصل على نوع الاستجابة الصحيح:</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  route</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'input'</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\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'output'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]> {</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // implementation below</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'GET /orders/:id'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });        </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Order</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> page</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'GET /orders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });    </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Page&#x3C;Order></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'POST /orders/:id/cancel'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type '{ id: number; }' is not assignable to parameter</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// of type '{ id: number; reason: string; }'.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">//   Property 'reason' is missing in type '{ id: number; }' but required in type …</span></span></code></pre></div>\n<p>كل فكرة من هذا القسم تعمل هنا. <code dir=\"ltr\">R</code> يظهر ثلاث مرات، رابطًا المسار بالمدخل والمخرج. قيده، <code dir=\"ltr\">keyof Routes</code>، هو union من قيم نصية حرفية، فيُستنتج الوسيط على أنه القيمة الحرفية <code dir=\"ltr\">'GET /orders/:id'</code> لا <code dir=\"ltr\">string</code>. ولـ <code dir=\"ltr\">R</code> موقع استنتاج واحد فقط، هو <code dir=\"ltr\">route</code>؛ أما <code dir=\"ltr\">input</code> فـ<em>يستخدم</em> <code dir=\"ltr\">R</code> فقط، عبر الوصول بالفهرس <code dir=\"ltr\">Routes[R]['input']</code>. يستنتج TypeScript المسار، ثم يفحص المدخل مقابل ما يحتاجه ذلك المسار. لا حاجة إلى <code dir=\"ltr\">NoInfer</code>، لأنه لا يوجد ما يمكن أن يساهم بمرشّح آخر.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-at-2-4-t1\"><defs><marker id=\"arrow-l-at-2-4-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-2-4-t1\">يمرّر موضع الاستدعاء نص مسار وكائن مدخل. يُستنتج نص المسار على أنه المفتاح الحرفي R. يفهرس R الخريطة Routes، فتنتج نوع المدخل الذي يُفحص به وسيط المدخل، ونوع المخرج المستخدَم للـ promise المُعاد.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"20\" width=\"250\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"135\" y=\"47\" text-anchor=\"middle\">'GET /orders/:id'</text>\n  <rect class=\"d-box\" x=\"10\" y=\"186\" width=\"250\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"135\" y=\"213\" text-anchor=\"middle\">{ id: 1042 }</text>\n  <path class=\"d-arrow\" d=\"M260 42 L318 42\" marker-end=\"url(#arrow-l-at-2-4-1)\"/>\n  <rect class=\"d-box-primary\" x=\"322\" y=\"20\" width=\"120\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"382\" y=\"47\" text-anchor=\"middle\">R</text>\n  <path class=\"d-arrow\" d=\"M382 64 L382 98\" marker-end=\"url(#arrow-l-at-2-4-1)\"/>\n  <rect class=\"d-box-accent\" x=\"292\" y=\"102\" width=\"180\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"382\" y=\"135\" text-anchor=\"middle\">Routes[R]</text>\n  <path class=\"d-arrow\" d=\"M292 150 L264 196\" marker-end=\"url(#arrow-l-at-2-4-1)\"/>\n  <text class=\"d-code\" x=\"190\" y=\"170\" text-anchor=\"middle\">['input'] يفحص</text>\n  <path class=\"d-arrow\" d=\"M472 130 L520 130\" marker-end=\"url(#arrow-l-at-2-4-1)\"/>\n  <rect class=\"d-box-success\" x=\"524\" y=\"102\" width=\"166\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"607\" y=\"128\" text-anchor=\"middle\">['output']</text>\n  <text class=\"d-label-muted\" x=\"607\" y=\"148\" text-anchor=\"middle\">Promise&lt;Order&gt;</text>\n</svg><figcaption>يُستنتج مفتاح المسار مرة واحدة؛ ويُبحث عن المدخل والمخرج انطلاقًا منه</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">غير آمن من الداخل، آمن من الخارج</h2>\n<p>داخل <code dir=\"ltr\">call</code>، نوع <code dir=\"ltr\">input</code> هو <code dir=\"ltr\">Routes[R]['input']</code> لـ <code dir=\"ltr\">R</code> ما مجهول. لا يستطيع TypeScript معرفة الحقول التي فيه، فبناء الرابط يحتاج إلى تحويل. وهذا مقبول، ما دام يحدث في مكان واحد بالضبط:</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\"> Transport</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">method</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\">path</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\">body</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\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">unknown</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\"> createClient</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">transport</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Transport</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:#CF222E;--shiki-dark:#FF7B72\"> async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    route</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'input'</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\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'output'</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:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">method</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">template</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> route.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">split</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</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:#6E7781;--shiki-dark:#8B949E\">    // The route map guarantees every :param has a matching input field.</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> values</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> input </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</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\">unknown</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\"> rest</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\">unknown</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:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">values };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> path</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> template.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">replace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/:(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\w</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)/</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">g</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</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:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      delete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rest[name];</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> encodeURIComponent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(values[name]));</span></span>\n<span class=\"line\"><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\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> transport</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(method, path, method </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'GET'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rest);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> result </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'output'</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>هناك تحويلان، كلاهما مشروح بتعليق، وكلاهما في الدالة الوحيدة التي يمرّ بها كل طلب. يحصل المستدعون على API مفحوص بالكامل، والمراجع الذي يريد تدقيق الأجزاء غير الآمنة يقرأ عشرة أسطر. قارن ذلك بقاعدة كود يقول فيها كل موضع من مئتي موضع استدعاء <code dir=\"ltr\">as Order</code>.</p>\n<p>حقن الـ <code dir=\"ltr\">transport</code> قرار تصميمي يستحق التقليد. التطبيق الحقيقي يمرّر دالة تستدعي <code dir=\"ltr\">fetch</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\">Promise&lt;Routes[R]['output']&gt;</code> وعدٌ <em>تقطعه أنت</em> بشأن الخادم، والـ <code dir=\"ltr\">as</code> الأخيرة هي حيث تقطعه. إذا أعادت الواجهة الخلفية تسمية <code dir=\"ltr\">status</code> إلى <code dir=\"ltr\">state</code>، فسيظل كل شيء يمرّ في الترجمة، ويظهر الفشل كـ <code dir=\"ltr\">undefined</code> على بعد ثلاثة components. الأنواع تنظّم افتراضاتك؛ لا تتحقّق منها. في <a href=\"/ar/lesson/advanced-typescript/l-at-4-3\">تحليل البيانات غير الموثوقة عند الحدود</a> ستضيف محلّلًا لكل مسار، فيُكتسب نوع المخرج وقت التشغيل بدل أن يُدّعى.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">توسيع الخريطة</h2>\n<p>للعملاء الحقيقيين endpoints لا تأخذ شيئًا، مثل <code dir=\"ltr\">'GET /me'</code>. مع هذا التصميم تعطيها <code dir=\"ltr\">input: {}</code> ويكتب المستدعون <code dir=\"ltr\">call('GET /me', {})</code>. ذلك الكائن الفارغ عيبٌ صغير، وهو مقايضة عادلة حاليًا: البديل توقيع يجعل <code dir=\"ltr\">input</code> اختياريًا لبعض المسارات فقط، وهذا يحتاج إلى conditional type. ستملك تلك الأداة بعد <a href=\"/ar/lesson/advanced-typescript/l-at-3-3\">الـ Conditional Types والتوزيع و infer</a>، ولن يمسّ التغيير إلا التوقيع، لا موضع استدعاء واحدًا.</p>\n<p>والأمر نفسه مع سلاسل الاستعلام مقابل أجسام الطلبات، أو الترويسات، أو أنواع الأخطاء لكل مسار. كلٌّ منها خاصية إضافية في قيم خريطة المسارات وعملية بحث إضافية في التوقيع. ويبقى شكل الحل كما هو: صِفه مرة واحدة في الجدول، واشتقّه في كل مكان آخر.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">ما الذي يجعل هذا التصميم جيدًا</h2>\n<p>انظر إلى ما لا يضطر المستدعي إلى فعله أبدًا: كتابة <code dir=\"ltr\">&lt;Order&gt;</code>، أو التحويل، أو استيراد أنواع الاستجابات، أو تذكّر أيّ endpoints تحتاج أيّ حقول. أفضل الـ APIs الـ generic تبدو كأنها بلا أنواع إطلاقًا، لأن كل نوع فيها مستنتج من شيء كان على المستدعي كتابته على أي حال (هنا، اسم المسار).</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>علامات على أن الـ API الـ generic يحتاج إلى إعادة تصميم</span></p>\n<p>مستدعون يكتبون وسائط نوع صريحة، ومستدعون يحوّلون النتائج، ومعاملات نوع لا تظهر إلا مرة واحدة: كلها أعراض. وتعني عادةً أن موقع الاستنتاج مفقود أو في المكان الخطأ. جِد الوسيط الواحد الذي يحدّد ما يريده المستدعي، واشتقّ كل ما عداه منه.</p>\n</aside>\n<p>في التمرين ستبني هذا العميل مقابل transport مزيّف. وبه ينتهي قسم الـ generics. القسم 3 يفكّك عوامل الأنواع (type operators) التي كنت تستعيرها (<code dir=\"ltr\">keyof</code> والوصول بالفهرس) ويضيف الـ mapped والـ conditional والـ template literal types، كي تشتقّ المزيد من مصدر حقيقة واحد.</p>\n","toc":[{"id":"sec-1","text":"مصدر حقيقة واحد: خريطة المسارات","level":2},{"id":"sec-2","text":"اسم المسار هو موقع الاستنتاج الوحيد","level":2},{"id":"sec-3","text":"غير آمن من الداخل، آمن من الخارج","level":2},{"id":"sec-4","text":"توسيع الخريطة","level":2},{"id":"sec-5","text":"ما الذي يجعل هذا التصميم جيدًا","level":2}],"takeaways":["ضع مدخل كل endpoint ومخرجه في نوع واحد هو خريطة المسارات، واشتقّ توقيعات العميل منه.","اجعل اسم المسار موقع الاستنتاج الوحيد؛ وكل ما عداه يُبحث عنه انطلاقًا منه بالوصول بالفهرس.","أبقِ التحويلات التي لا مفرّ منها داخل التنفيذ، في مكان واحد، كي يحصل المستدعون على API مفحوص بالكامل.","العميل محدَّد الأنواع ما زال يثق باستجابة الخادم؛ تحقّق منها عند الحدود قبل أن تعتمد على نوع المخرج المعلَن."],"further":[{"title":"Indexed Access Types","url":"https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html"},{"title":"Generic constraints","url":"https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints"},{"title":"Using the Fetch API (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"}],"quiz":[{"q":"<p>بالنظر إلى <code dir=\"ltr\">call&lt;R extends keyof Routes&gt;(route: R, input: Routes[R]['input']): Promise&lt;Routes[R]['output']&gt;</code>، كيف يُحدَّد <code dir=\"ltr\">R</code> في <code dir=\"ltr\">call('GET /orders/:id', { id: 1042 })</code>؟</p>\n","options":[{"text":"من الكائن <code dir=\"ltr\">input</code>، بمطابقة شكله مع كل مسار.","why":"نوع <code dir=\"ltr\">input</code> هو <code dir=\"ltr\">Routes[R]['input']</code>، الذي يعتمد على <code dir=\"ltr\">R</code>؛ يُفحص بعد أن يُعرف <code dir=\"ltr\">R</code>، ولا يُستخدم لإيجاده."},{"text":"من نص المسار، الذي يبقى القيمة الحرفية <code dir=\"ltr\">'GET /orders/:id'</code> لأن القيد union من قيم نصية حرفية.","why":"صحيح. تبقى القيمة الحرفية حرفية بفضل القيد <code dir=\"ltr\">keyof Routes</code>، وتُبحث الأنواع الأخرى انطلاقًا منها."},{"text":"من نوع الإرجاع المعلَن في موضع الاستدعاء.","why":"يمكن لـ TypeScript استخدام نوع إرجاع سياقي في بعض الحالات، لكن هنا وسيط المسار هو من يقرّر كل شيء."}],"answer":1},{"q":"<p>يحتاج تنفيذ <code dir=\"ltr\">call</code> إلى قراءة معاملات المسار من <code dir=\"ltr\">input</code>، ونوعه <code dir=\"ltr\">Routes[R]['input']</code>. ما النهج الأكثر معقولية؟</p>","options":[{"text":"اجعل التوقيع العام يأخذ <code dir=\"ltr\">input: any</code> كي يستطيع التنفيذ قراءة أي شيء.","why":"هذا يرمي الفحص الذي يعتمد عليه المستدعون، وهو الغاية من العميل أصلًا."},{"text":"اكتب overload لكل مسار كي يكون كل تنفيذ محدّدًا.","why":"هذا يكرّر خريطة المسارات ويجب تحديثه يدويًا مع كل endpoint جديد."},{"text":"أبقِ التوقيع العام generic وعامل <code dir=\"ltr\">input</code> كـ <code dir=\"ltr\">Record&lt;string, unknown&gt;</code> في الداخل، مع تحويل واحد مشروح بتعليق.","why":"صحيح. التحويل محلّي، يُراجَع مرة واحدة، وغير مرئي للمستدعين، الذين يحتفظون بالفحص الكامل."}],"answer":2},{"q":"<p>يعلن عميلك أن <code dir=\"ltr\">'GET /orders/:id'</code> يُعيد <code dir=\"ltr\">Order</code>. يطلق الخادم تغييرًا يعيد تسمية <code dir=\"ltr\">status</code> إلى <code dir=\"ltr\">state</code>. ماذا يفعل TypeScript؟</p>","options":[{"text":"لا شيء وقت الترجمة؛ يقرأ الكود <code dir=\"ltr\">order.status</code> فيحصل على <code dir=\"ltr\">undefined</code> وقت التشغيل، ويفشل في مكان ما لاحقًا.","why":"صحيح. خريطة المسارات وعدٌ بشأن الخادم لا يستطيع TypeScript فحصه. تحقّق من الاستجابات عند الحدود لتلتقط هذا مبكرًا."},{"text":"يبلّغ عن خطأ على سطر <code dir=\"ltr\">call</code> في المرة القادمة التي تبني فيها المشروع.","why":"لا يرى TypeScript استجابة الخادم أبدًا. إنه يفحص كودك فقط مقابل الأنواع التي أعلنتها."},{"text":"يرمي خطأ نوع وقت التشغيل داخل <code dir=\"ltr\">call</code>.","why":"تُمحى الأنواع؛ لا شيء يفحص الاستجابة وقت التشغيل ما لم تكتب ذلك الفحص بنفسك."}],"answer":0},{"q":"<p>لماذا يُفضَّل تحديد المسار كنص واحد مثل <code dir=\"ltr\">'POST /orders/:id/cancel'</code> بدل تمرير الطريقة والمسار كوسيطين منفصلين؟</p>","options":[{"text":"لأن الوسيطين لا يمكن أن يكونا كلاهما literal types.","why":"يمكن ذلك؛ لكل وسيط نوعه الحرفي الخاص. المشكلة في الحفاظ على اتساقهما."},{"text":"لأن المفتاح الواحد يعطي موقع استنتاج واحدًا يحدّد الـ endpoint، فلا يمكن أبدًا أن تتعارض الطريقة مع المسار.","why":"صحيح. مع وسيطين منفصلين ستحتاج إلى آليات إضافية لمنع اقتران <code dir=\"ltr\">POST</code> بمسار لا يقبل إلا GET."},{"text":"لأن <code dir=\"ltr\">fetch</code> تتطلّب الطريقة داخل الرابط.","why":"<code dir=\"ltr\">fetch</code> تأخذ الطريقة في كائن الخيارات. المفتاح الواحد خيار في تحديد الأنواع، لا متطلّب وقت التشغيل."}],"answer":1}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype Order = { id: number; status: OrderStatus; totalCents: number };\ntype CancelledOrder = { id: number; status: 'cancelled'; cancelReason: string };\ntype Page<T> = { items: T[]; nextCursor: string | null };\n\n// A fake server: records every request and returns canned data.\nconst sent: string[] = [];\nasync function transport(method: string, path: string, body: unknown): Promise<unknown> {\n  sent.push(`${method} ${path}${body === undefined ? '' : ' ' + JSON.stringify(body)}`);\n  if (path.endsWith('/cancel')) return { id: 1042, status: 'cancelled', cancelReason: 'Changed mind' };\n  if (path === '/orders') return { items: [], nextCursor: null };\n  return { id: 1042, status: 'processing', totalCents: 18900 };\n}\n\ntype Routes = {\n  'GET /orders/:id': { input: { id: number }; output: Order };\n  'GET /orders': { input: { status?: OrderStatus; cursor?: string }; output: Page<Order> };\n  // Add: POST /orders/:id/cancel takes { id, reason } and returns a CancelledOrder\n};\n\nasync function call(route: string, input: Record<string, unknown>): Promise<unknown> {\n  const [method, path] = route.split(' ');\n  // Path parameters like :id are not filled in yet.\n  return transport(method, path, method === 'GET' ? undefined : input);\n}\n\n// ---- Type tests (do not edit) ----\nasync function typeTests() {\n  const order = await call('GET /orders/:id', { id: 1042 });\n  type T1 = Expect<Equal<typeof order, Order>>;\n\n  const page = await call('GET /orders', { status: 'shipped' });\n  type T2 = Expect<Equal<typeof page, Page<Order>>>;\n\n  const cancelled = await call('POST /orders/:id/cancel', { id: 1042, reason: 'Changed mind' });\n  type T3 = Expect<Equal<typeof cancelled, CancelledOrder>>;\n\n  // @ts-expect-error: ids are numbers\n  await call('GET /orders/:id', { id: '1042' });\n  // @ts-expect-error: cancelling needs a reason\n  await call('POST /orders/:id/cancel', { id: 1042 });\n  // @ts-expect-error: there is no such route\n  await call('DELETE /orders/:id', { id: 1042 });\n}\n"},"solution":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype Order = { id: number; status: OrderStatus; totalCents: number };\ntype CancelledOrder = { id: number; status: 'cancelled'; cancelReason: string };\ntype Page<T> = { items: T[]; nextCursor: string | null };\n\n// A fake server: records every request and returns canned data.\nconst sent: string[] = [];\nasync function transport(method: string, path: string, body: unknown): Promise<unknown> {\n  sent.push(`${method} ${path}${body === undefined ? '' : ' ' + JSON.stringify(body)}`);\n  if (path.endsWith('/cancel')) return { id: 1042, status: 'cancelled', cancelReason: 'Changed mind' };\n  if (path === '/orders') return { items: [], nextCursor: null };\n  return { id: 1042, status: 'processing', totalCents: 18900 };\n}\n\ntype Routes = {\n  'GET /orders/:id': { input: { id: number }; output: Order };\n  'GET /orders': { input: { status?: OrderStatus; cursor?: string }; output: Page<Order> };\n  'POST /orders/:id/cancel': { input: { id: number; reason: string }; output: CancelledOrder };\n};\n\nasync function call<R extends keyof Routes>(\n  route: R,\n  input: Routes[R]['input'],\n): Promise<Routes[R]['output']> {\n  const [method, template] = route.split(' ');\n  // The route map guarantees every :param has a matching input field.\n  const values = input as Record<string, unknown>;\n  const rest: Record<string, unknown> = { ...values };\n  const path = template.replace(/:(\\w+)/g, (_, name: string) => {\n    delete rest[name];\n    return encodeURIComponent(String(values[name]));\n  });\n  const result = await transport(method, path, method === 'GET' ? undefined : rest);\n  return result as Routes[R]['output'];\n}\n\n// ---- Type tests (do not edit) ----\nasync function typeTests() {\n  const order = await call('GET /orders/:id', { id: 1042 });\n  type T1 = Expect<Equal<typeof order, Order>>;\n\n  const page = await call('GET /orders', { status: 'shipped' });\n  type T2 = Expect<Equal<typeof page, Page<Order>>>;\n\n  const cancelled = await call('POST /orders/:id/cancel', { id: 1042, reason: 'Changed mind' });\n  type T3 = Expect<Equal<typeof cancelled, CancelledOrder>>;\n\n  // @ts-expect-error: ids are numbers\n  await call('GET /orders/:id', { id: '1042' });\n  // @ts-expect-error: cancelling needs a reason\n  await call('POST /orders/:id/cancel', { id: 1042 });\n  // @ts-expect-error: there is no such route\n  await call('DELETE /orders/:id', { id: 1042 });\n}\n"},"checks":[{"id":"types","typecheck":true},{"id":"fills-params","test":"sent.length = 0; await call('GET /orders/:id', { id: 1042 }); return sent[0] === 'GET /orders/1042';"},{"id":"post-body","test":"sent.length = 0; const r = await call('POST /orders/:id/cancel', { id: 7, reason: 'Late' }); return sent[0] === 'POST /orders/7/cancel {\"reason\":\"Late\"}' && r.status === 'cancelled';"}],"title":"حدّد أنواع عميل الدفع من خريطة مسارات","prompt":"<p>ابنِ الدالة <code dir=\"ltr\">call</code> محدَّدة الأنواع في Cartwheel.</p>\n<ol>\n<li>أضف المسار <code dir=\"ltr\">'POST /orders/:id/cancel'</code> إلى <code dir=\"ltr\">Routes</code>: مدخله <code dir=\"ltr\">{ id: number; reason: string }</code> ومخرجه <code dir=\"ltr\">CancelledOrder</code>.</li>\n<li>اجعل <code dir=\"ltr\">call</code> generic على اسم المسار، بحيث يُبحث عن نوع <code dir=\"ltr\">input</code> ونوع القيمة المحلولة في <code dir=\"ltr\">Routes</code>. اختبارات الأنواع تُظهر ما يتوقّعه المستدعون: بلا وسائط نوع، وبلا تحويلات.</li>\n<li>وقت التشغيل، استبدل كل <code dir=\"ltr\">:param</code> في المسار بقيمة المدخل المطابقة، وأرسل الحقول المتبقّية كجسم للطلبات غير GET. يجب أن ترسل <code dir=\"ltr\">call('POST /orders/:id/cancel', { id: 7, reason: 'Late' })</code> الطلب <code dir=\"ltr\">POST /orders/7/cancel</code> بالجسم <code dir=\"ltr\">{&quot;reason&quot;:&quot;Late&quot;}</code>.</li>\n</ol>\n","hints":["التوقيع هو <code dir=\"ltr\">call&lt;R extends keyof Routes&gt;(route: R, input: Routes[R]['input']): Promise&lt;Routes[R]['output']&gt;</code>.","داخل الدالة، عامل <code dir=\"ltr\">input</code> كـ <code dir=\"ltr\">Record&lt;string, unknown&gt;</code> بتحويل واحد مشروح بتعليق، وحوّل نتيجة الـ transport إلى <code dir=\"ltr\">Routes[R]['output']</code> مرة واحدة.","<code dir=\"ltr\">template.replace(/:(\\w+)/g, (_, name) =&gt; …)</code> تعطيك اسم كل معامل؛ انسخ المدخل بـ <code dir=\"ltr\">{ ...values }</code> واحذف (<code dir=\"ltr\">delete</code>) الأسماء التي استخدمتها، فيكون المتبقّي هو الجسم."],"explanation":"<p>لـ <code dir=\"ltr\">R</code> موقع استنتاج واحد، هو نص المسار، وقيده يُبقيه مفتاحًا حرفيًا. وكل نوع آخر وصولٌ بالفهرس على <code dir=\"ltr\">Routes</code>، فإضافة endpoint تغيير بسطر واحد في الخريطة، ويُفحص كل مستدعٍ مقابلها. التحويلان يعيشان داخل <code dir=\"ltr\">call</code>، موثّقين، بدل أن يكونا في كل موضع استدعاء. ولاحظ أن نوع المخرج ما زال ادّعاءً بشأن الخادم: الـ transport المزيّف يُعيد ما تعد به الخريطة، أما الحقيقي فلا يمكن الوثوق به دون تحقّق.</p>\n","checkLabels":{"types":"الملف يجتاز فحص الأنواع بلا أي خطأ، بما فيها كل اختبارات الأنواع","fills-params":"تُملأ معاملات المسار: <code dir=\"ltr\">GET /orders/1042</code>","post-body":"مسار الإلغاء يرسل إلى <code dir=\"ltr\">/orders/7/cancel</code> والجسم فيه الحقول المتبقّية فقط"}},"words":656,"readMinutes":4}