{"id":"l-at-4-3","courseId":"advanced-typescript","lang":"ar","kind":"lesson","summary":"عامل كل ما يأتي من خارج برنامجك على أنه unknown، وحوّله إلى أنواع مجال موثوقة بالمحلّلات والـ type predicates ودوال التأكيد، وأبقِ التحويلات خارج كود القلب.","html":"<p>في <a href=\"/ar/lesson/advanced-typescript/l-at-2-4\">تصميم عميل API بأنواع دقيقة</a>، انتهى العميل بـ <code dir=\"ltr\">return result as Routes[R]['output']</code>. ذلك التحويل كان وعدًا بشأن الخادم. وهذا الدرس عن إبقاء ذلك الوعد صادقًا.</p>\n<p>لكل برنامج <strong>حدود (boundary)</strong>: المواضع التي تأتي منها البيانات من مكان لا يستطيع الـ compiler رؤيته. استجابات HTTP، و<code dir=\"ltr\">JSON.parse</code>، ومعاملات الروابط، و<code dir=\"ltr\">localStorage</code>، وحقول النماذج، والرسائل من نوافذ أخرى، ومتغيّرات البيئة. داخل الحدود، تصف الأنواع قيمًا أنشأها كودك. وعند الحدود، الأنواع مجرّد آمال. وللنمط الذي يتعامل مع ذلك شعار معروف: <strong>حلّل، لا تتحقّق فقط (parse, don’t validate)</strong>. حوّل البيانات غير المحدَّدة الأنواع إلى بيانات محدَّدة الأنواع مرة واحدة، عند الحافة، ودع بقية البرنامج يثق بالأنواع.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-at-4-3-t1\"><defs><marker id=\"arrow-l-at-4-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-at-4-3-t1\">على اليسار، مصادر من خارج البرنامج: استجابات fetch، وJSON.parse، ومعاملات الروابط، وlocalStorage، كلها من نوع unknown. تمرّ عبر محلّلات عند الحدود، تُعيد إمّا خطأً أو Order محدَّد النوع. وداخل الحدود، لا يرى كود الدفع الأساسي إلا أنواعًا موثوقة.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"20\" width=\"190\" height=\"180\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"105\" y=\"46\" text-anchor=\"middle\">الخارج: unknown</text>\n  <text class=\"d-code\" x=\"105\" y=\"80\" text-anchor=\"middle\">res.json()</text>\n  <text class=\"d-code\" x=\"105\" y=\"108\" text-anchor=\"middle\">JSON.parse</text>\n  <text class=\"d-code\" x=\"105\" y=\"136\" text-anchor=\"middle\">معاملات URL</text>\n  <text class=\"d-code\" x=\"105\" y=\"164\" text-anchor=\"middle\">localStorage</text>\n  <path class=\"d-arrow\" d=\"M200 110 L256 110\" marker-end=\"url(#arrow-l-at-4-3-1)\"/>\n  <rect class=\"d-box-warn\" x=\"260\" y=\"70\" width=\"170\" height=\"80\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"345\" y=\"104\" text-anchor=\"middle\">parseOrder()</text>\n  <text class=\"d-label-muted\" x=\"345\" y=\"128\" text-anchor=\"middle\">افحص، ثم سِم</text>\n  <path class=\"d-arrow\" d=\"M345 150 L345 186\" marker-end=\"url(#arrow-l-at-4-3-1)\"/>\n  <text class=\"d-code\" x=\"345\" y=\"206\" text-anchor=\"middle\">err(…)</text>\n  <path class=\"d-arrow\" d=\"M430 110 L486 110\" marker-end=\"url(#arrow-l-at-4-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"490\" y=\"20\" width=\"200\" height=\"180\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"590\" y=\"46\" text-anchor=\"middle\">الداخل: موثوق</text>\n  <text class=\"d-code\" x=\"590\" y=\"100\" text-anchor=\"middle\">Order</text>\n  <text class=\"d-label-muted\" x=\"590\" y=\"128\" text-anchor=\"middle\">بلا تحويلات ولا فحوص</text>\n</svg><figcaption>تصبح البيانات غير الموثوقة بيانات محدَّدة الأنواع عند بوابة واحدة</figcaption></figure>\n<h2 id=\"sec-1\" tabindex=\"-1\">ابدأ من unknown</h2>\n<p><code dir=\"ltr\">JSON.parse</code> تُعيد <code dir=\"ltr\">any</code>، وكذلك <code dir=\"ltr\">Response.json()</code>. و<code dir=\"ltr\">any</code> معدية: أسندها إلى متغيّر نوعه <code dir=\"ltr\">Order</code> فيصدّقك TypeScript. الخطوة الأولى أن ترفض هذه الهدية:</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\"> raw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> unknown</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">json</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">raw.status;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: 'raw' is of type 'unknown'.</span></span></code></pre></div>\n<p><code dir=\"ltr\">unknown</code> هو النوع الصادق للبيانات التي لم تفحصها. يمكنك تمريرها، لكن لا يمكنك استخدامها حتى تضيّقها. والتضييق هو ما يفعله المحلّل.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">محلّل مكتوب يدويًا</h2>\n<p>يأخذ المحلّل <code dir=\"ltr\">unknown</code> ويُعيد إمّا قيمة محدَّدة النوع أو وصفًا لما كان خاطئًا. مع نوع <code dir=\"ltr\">Result</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\"> STATUSES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'delivered'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'cancelled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'returned'</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\"> OrderStatus</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\"> STATUSES</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:#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 style=\"color:#953800;--shiki-dark:#FFA657\">couponCode</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> isRecord</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\"> 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\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> is</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\">  return</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> value </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'object'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> value </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Array.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isArray</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(value);</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> isOrderStatus</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\"> 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\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> is</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\">  return</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> value </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'string'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STATUSES</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>\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\"> parseOrder</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:#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\"> Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isRecord</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> err</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'order: expected an object'</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\">id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">status</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">totalCents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">couponCode</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> input;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'number'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ||</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isInteger</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(id)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> err</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'id: expected an integer'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isOrderStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(status)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> err</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`status: unknown value ${</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">status</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> totalCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'number'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> err</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'totalCents: expected a number'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (couponCode </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> couponCode </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'string'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> err</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'couponCode: expected a string or null'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> ok</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ id, status, totalCents, couponCode });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>انظر إلى السطر الأخير: لا يوجد تحويل. كل فحص ضيّق متغيّرًا واحدًا، وفي النهاية يستطيع TypeScript التحقّق من أن الكائن الحرفي يطابق <code dir=\"ltr\">Order</code>. وإن اكتسب النوع حقلًا ونسيه المحلّل، يفشل سطر <code dir=\"ltr\">ok(…)</code> في الترجمة. وهذا أيضًا هو المكان الذي تسِم فيه القيم، باستدعاء <code dir=\"ltr\">toOrderId(id)</code> من <a href=\"/ar/lesson/advanced-typescript/l-at-4-1\">الـ Branded Types والفحوص الشاملة</a>.</p>\n<h3 id=\"sec-3\" tabindex=\"-1\">سدّ الفجوة في عميل الـ API</h3>\n<p>عودةً إلى العميل، إصلاح ذلك التحويل الأخير هو جدول من المحلّلات، واحد لكل مسار، يُفحص مقابل خريطة المسارات بـ mapped type و<code dir=\"ltr\">satisfies</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\"> parsers</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:#1F2328;--shiki-dark:#E6EDF3\">: parseOrder,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'GET /orders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: parseOrderPage,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'POST /orders/:id/cancel'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: parseCancelledOrder,</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:#1F2328;--shiki-dark:#E6EDF3\"> { [</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</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 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:#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\"> Result</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 style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> };</span></span></code></pre></div>\n<p>انسَ مسارًا فتبلّغ <code dir=\"ltr\">satisfies</code> عن المفتاح الناقص؛ وأعِد النوع الخاطئ من محلّل فتبلّغ عن ذلك أيضًا. داخل <code dir=\"ltr\">call</code>، تمرّ الاستجابة الآن عبر <code dir=\"ltr\">parsers[route]</code> قبل إعادتها. لا يستطيع TypeScript ربط الـ <code dir=\"ltr\">R</code> العام بالصف المطابق في الجدول، فيبقى تعليق نوعي واحد هناك، لكنه يقع الآن على بيانات فُحصت فعلًا: أنواع مخرجات العميل مكتسَبة لا مُدّعاة.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">الـ type predicates</h2>\n<p><code dir=\"ltr\">isRecord</code> و<code dir=\"ltr\">isOrderStatus</code> هما <strong>type predicates (دوال حارسة للنوع)</strong>: دوال نوع إرجاعها <code dir=\"ltr\">value is T</code>. حين تُعيد إحداها <code dir=\"ltr\">true</code>، يضيّق TypeScript الوسيط في موضع الاستدعاء. ومنذ TypeScript 5.5، تُستنتج البسيطة منها مثل <code dir=\"ltr\">(x) =&gt; x !== null</code>، كما رأيت في القسم 1، لكن أي شيء فيه منطق حقيقي يحتاج إلى التعليق النوعي.</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>الـ predicates التي تكذب</span></p>\n<p>لا يتحقّق TypeScript من أن جسم الـ predicate يثبت ادّعاءه. <code dir=\"ltr\">function isOrder(v: unknown): v is Order { return isRecord(v) &amp;&amp; 'id' in v; }</code> تمرّ في الترجمة، وكل مستدعٍ يصدّق الآن أن سجلّ العميل طلب. الـ predicate تحويل <code dir=\"ltr\">as</code> ملفوف في دالة. أبقِ الـ predicates صغيرة وصحيحة بشكل واضح، مثل <code dir=\"ltr\">isOrderStatus</code>، وابنِ الفحوص الأكبر منها في محلّلات تُنشئ القيمة، كي يتحقّق الـ compiler من النتيجة.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">دوال التأكيد</h2>\n<p>أحيانًا تريد التضييق والتوقّف إن كانت البيانات سيئة، دون <code dir=\"ltr\">if</code> في كل موضع استدعاء. <strong>دالة التأكيد (assertion function)</strong> لا تُعيد شيئًا وترمي خطأً عند الفشل، ونوع إرجاعها <code dir=\"ltr\">asserts</code> يضيّق كل ما يلي الاستدعاء:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> assertIsOrder</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:#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:#CF222E;--shiki-dark:#FF7B72\"> asserts</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> is</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:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> parsed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> parseOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">parsed.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Invalid order: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">parsed</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">error</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> raw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> unknown</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cachedJson);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">assertIsOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(raw);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">raw.status; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// OrderStatus</span></span></code></pre></div>\n<p>تناسب دوال التأكيد الأماكن التي تكون فيها البيانات السيئة خطأً برمجيًا لا حالة متوقَّعة: دوال الاختبار المساعدة، والبيانات التي عرضها خادمك في الصفحة، والثوابت. أما لاستجابات HTTP، ففضّل محلّلًا يُعيد <code dir=\"ltr\">Result</code>، كي ينتج تغيّر الـ API خطأً مُعالَجًا لا انهيارًا. وهناك غرابة واحدة: يجب أن تُستدعى دالة التأكيد عبر اسم له نوع صريح، فـ <code dir=\"ltr\">const assertIsOrder = (x: unknown): asserts x is Order =&gt; …</code> تحتاج إلى تعليق نوعي على الـ <code dir=\"ltr\">const</code> نفسه، وإلا يبلّغ TypeScript عن “Assertions require every name in the call target to be declared with an explicit type annotation”.</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>استخدم مكتبة مخطّطات في المشاريع الحقيقية</span></p>\n<p>المحلّلات المكتوبة يدويًا مثالية للتعلّم ومقبولة لأنواع قليلة. أما لـ API كامل، فاستخدم مكتبة مخطّطات (schema) مثل Zod أو Valibot أو ArkType: تعلن الشكل مرة واحدة كقيمة وقت التشغيل، والمكتبة تفحص البيانات وتعطيك نوع TypeScript المشتقّ منه، فلا يمكن للمحلّل والنوع أن يتباعدا. إنها فكرة “مصدر الحقيقة الواحد” نفسها على طريقة <code dir=\"ltr\">typeof</code> من القسم 3، مغلّفة لك.</p>\n</aside>\n<p>في التمرين ستكتب محلّل الطلبات في Cartwheel وpredicate ودالة تأكيد، وتستبدل تحويلًا ظل يكذب منذ اليوم الأول. والدرس القادم يبني باعثات أحداث وبنّائين محدَّدي الأنواع فوق كل ما في هذا القسم.</p>\n","toc":[{"id":"sec-1","text":"ابدأ من unknown","level":2},{"id":"sec-2","text":"محلّل مكتوب يدويًا","level":2},{"id":"sec-3","text":"سدّ الفجوة في عميل الـ API","level":3},{"id":"sec-4","text":"الـ type predicates","level":2},{"id":"sec-5","text":"دوال التأكيد","level":2}],"takeaways":["<code dir=\"ltr\">JSON.parse</code> و<code dir=\"ltr\">response.json()</code> و<code dir=\"ltr\">localStorage</code> تعطيك <code dir=\"ltr\">any</code>؛ علّق نتائجها بـ <code dir=\"ltr\">unknown</code> كي يجبرك الـ compiler على الفحص.","المحلّل يأخذ <code dir=\"ltr\">unknown</code> ويُعيد قيمة محدَّدة النوع أو خطأً، فلا يرى بقية الكود إلا بيانات فُحصت.","الـ type predicate (<code dir=\"ltr\">value is T</code>) يضيّق النوع في موضع الاستدعاء، لكن TypeScript يثق بجسمه، فالـ predicate الخاطئ تحويل غير مفحوص.","دالة التأكيد (<code dir=\"ltr\">asserts value is T</code>) تضيّق كل ما يلي الاستدعاء، ويجب أن ترمي خطأً حين يفشل الفحص.","مكتبات المخطّطات تعلن الفحص مرة واحدة وتشتقّ النوع منه، فتُزيل خطر التباعد بين المحلّل والنوع."],"further":[{"title":"Using type predicates","url":"https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates"},{"title":"Assertion functions (TypeScript 3.7 release notes)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html"},{"title":"JSON.parse() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse"}],"quiz":[{"q":"<p><code dir=\"ltr\">const data = await res.json();</code> ما نوع <code dir=\"ltr\">data</code>، وماذا يجب أن تفعل حياله؟</p>","options":[{"text":"<code dir=\"ltr\">unknown</code>؛ ضيّقه قبل الاستخدام.","why":"<code dir=\"ltr\">Response.json()</code> معلَنة على أنها تُعيد <code dir=\"ltr\">Promise&lt;any&gt;</code>، لا <code dir=\"ltr\">unknown</code>، فلا شيء يجبرك على التضييق ما لم تعلّقها بنوع."},{"text":"<code dir=\"ltr\">Order</code>، مستنتَجًا من رابط الـ endpoint.","why":"لا يستطيع TypeScript معرفة ما يُعيده الخادم؛ الرابط بالنسبة إليه مجرّد نص."},{"text":"<code dir=\"ltr\">object</code>؛ حوّله بـ <code dir=\"ltr\">as Order</code>.","why":"النوع <code dir=\"ltr\">any</code>، والتحويل يتخطّى الفحص الذي كان سيلتقط تغيّر الـ API."},{"text":"<code dir=\"ltr\">any</code>؛ علّقه بـ <code dir=\"ltr\">unknown</code> ومرّره عبر محلّل.","why":"صحيح. <code dir=\"ltr\">any</code> تُطفئ الفحص في كل استخدام؛ و<code dir=\"ltr\">unknown</code> تجبر كل استخدام على أن يُثبَت أولًا."}],"answer":3},{"q":"<p>ما الخطأ في هذا الـ predicate؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> isOrder</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\"> 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\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> is</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:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> value </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'object'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> value </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'id'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</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","options":[{"text":"لا شيء؛ فحص <code dir=\"ltr\">id</code> يكفي لإثبات أن القيمة <code dir=\"ltr\">Order</code>.","why":"سجلّ العميل فيه <code dir=\"ltr\">id</code> أيضًا. الـ predicate يدّعي أكثر بكثير مما يفحص."},{"text":"يدّعي أن القيمة <code dir=\"ltr\">Order</code> كامل لكنه لا يفحص إلا <code dir=\"ltr\">id</code>، ويثق TypeScript بالادّعاء دون التحقّق منه.","why":"صحيح. جسم الـ predicate لا يُفحص مقابل نوع إرجاعه، فالفحص الضعيف تحويل غير مفحوص."},{"text":"الـ predicates لا يمكنها استخدام المعامل <code dir=\"ltr\">in</code>.","why":"<code dir=\"ltr\">in</code> فحص تضييق عادي ويعمل جيدًا داخل الـ predicates."}],"answer":1},{"q":"<p>كيف تختلف دالة التأكيد عن الـ type predicate؟</p>","options":[{"text":"دالة التأكيد تُعيد <code dir=\"ltr\">boolean</code>؛ والـ predicate يُعيد <code dir=\"ltr\">void</code>.","why":"العكس هو الصحيح. الـ predicate يُعيد <code dir=\"ltr\">boolean</code>؛ ودالة التأكيد لا تُعيد شيئًا وترمي خطأً عند الفشل."},{"text":"دالة التأكيد تضيّق القيمة لبقية النطاق بعد الاستدعاء، وترمي خطأً إن فشل الفحص.","why":"صحيح. <code dir=\"ltr\">assertIsOrder(x); x.id</code> تعمل، لأن العودة الطبيعية تثبت التأكيد."},{"text":"دوال التأكيد لا تعمل إلا على الأنواع الأولية.","why":"تعمل على أي نوع، بما فيه الكائنات والـ unions."}],"answer":1},{"q":"<p>أين يجب تحليل القيمة القادمة من <code dir=\"ltr\">localStorage.getItem('cart')</code>؟</p>","options":[{"text":"حيث تُقرأ بالضبط، عند الحدود، قبل أن يراها أي كود آخر.","why":"صحيح. حلّل مرة واحدة عند الحافة، فتستطيع كل دالة داخل التطبيق الوثوق بالنوع."},{"text":"في كل component يستخدم السلة، قبل الـ render مباشرة.","why":"التحليل في أماكن كثيرة يكرّر المنطق ويترك نوافذ تتدفّق فيها بيانات غير مفحوصة."},{"text":"في أي مكان؛ تطبيقك هو من كتبها، فشكلها مضمون.","why":"الإصدارات القديمة من تطبيقك، وإضافات المتصفّح، والمستخدمون بأدوات المطوّر، كلهم يكتبون في التخزين. عاملها على أنها غير موثوقة."},{"text":"في الـ reducer الذي يخزّن السلة، بعد استخدامها في أول render.","why":"تكون البيانات غير المفحوصة قد عُرضت حينها؛ الحدود هي أول قراءة، لا خطوة لاحقة."}],"answer":0}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };\nconst ok = <T>(value: T): { ok: true; value: T } => ({ ok: true, value });\nconst err = <E>(error: E): { ok: false; error: E } => ({ ok: false, error });\n\nconst STATUSES = ['processing', 'shipped', 'delivered', 'cancelled', 'returned'] as const;\ntype OrderStatus = (typeof STATUSES)[number];\ntype Order = { id: number; status: OrderStatus; totalCents: number; couponCode: string | null };\n\nfunction isRecord(value: unknown): boolean {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isOrderStatus(value: unknown): boolean {\n  return typeof value === 'string' && (STATUSES as readonly string[]).includes(value);\n}\n\n// This \"parser\" trusts whatever the server sends.\nfunction parseOrder(input: unknown): Result<Order, string> {\n  return ok(input as Order);\n}\n\nfunction assertIsOrder(input: unknown): void {\n  const parsed = parseOrder(input);\n  if (!parsed.ok) throw new Error(`Invalid order: ${parsed.error}`);\n}\n\n// ---- Type tests (do not edit) ----\nfunction typeTests(x: unknown, y: unknown) {\n  if (isRecord(x)) {\n    type T1 = Expect<Equal<typeof x, Record<string, unknown>>>;\n  }\n  if (isOrderStatus(x)) {\n    type T2 = Expect<Equal<typeof x, OrderStatus>>;\n  }\n  assertIsOrder(y);\n  type T3 = Expect<Equal<typeof y, Order>>;\n}\n\nconst fromServer: unknown = JSON.parse('{\"id\":1042,\"status\":\"refunded\",\"totalCents\":18900,\"couponCode\":null}');\nconst parsed = parseOrder(fromServer);\nconsole.log(parsed.ok ? parsed.value : parsed.error);\n"},"solution":{"ts":"type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };\nconst ok = <T>(value: T): { ok: true; value: T } => ({ ok: true, value });\nconst err = <E>(error: E): { ok: false; error: E } => ({ ok: false, error });\n\nconst STATUSES = ['processing', 'shipped', 'delivered', 'cancelled', 'returned'] as const;\ntype OrderStatus = (typeof STATUSES)[number];\ntype Order = { id: number; status: OrderStatus; totalCents: number; couponCode: string | null };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isOrderStatus(value: unknown): value is OrderStatus {\n  return typeof value === 'string' && (STATUSES as readonly string[]).includes(value);\n}\n\nfunction parseOrder(input: unknown): Result<Order, string> {\n  if (!isRecord(input)) return err('order: expected an object');\n  const { id, status, totalCents, couponCode } = input;\n  if (typeof id !== 'number' || !Number.isInteger(id)) return err('id: expected an integer');\n  if (!isOrderStatus(status)) return err(`status: unknown value ${JSON.stringify(status)}`);\n  if (typeof totalCents !== 'number' || !Number.isInteger(totalCents)) return err('totalCents: expected whole cents');\n  if (couponCode !== null && typeof couponCode !== 'string') return err('couponCode: expected a string or null');\n  return ok({ id, status, totalCents, couponCode });\n}\n\nfunction assertIsOrder(input: unknown): asserts input is Order {\n  const parsed = parseOrder(input);\n  if (!parsed.ok) throw new Error(`Invalid order: ${parsed.error}`);\n}\n\n// ---- Type tests (do not edit) ----\nfunction typeTests(x: unknown, y: unknown) {\n  if (isRecord(x)) {\n    type T1 = Expect<Equal<typeof x, Record<string, unknown>>>;\n  }\n  if (isOrderStatus(x)) {\n    type T2 = Expect<Equal<typeof x, OrderStatus>>;\n  }\n  assertIsOrder(y);\n  type T3 = Expect<Equal<typeof y, Order>>;\n}\n\nconst fromServer: unknown = JSON.parse('{\"id\":1042,\"status\":\"refunded\",\"totalCents\":18900,\"couponCode\":null}');\nconst parsed = parseOrder(fromServer);\nconsole.log(parsed.ok ? parsed.value : parsed.error);\n"},"checks":[{"id":"types","typecheck":true},{"id":"accepts-valid","test":"const r = parseOrder({ id: 1042, status: 'shipped', totalCents: 18900, couponCode: 'LOYAL5' }); return r.ok === true && r.value.status === 'shipped' && r.value.couponCode === 'LOYAL5';"},{"id":"rejects-status","test":"const r = parseOrder({ id: 1042, status: 'refunded', totalCents: 18900, couponCode: null }); return r.ok === false && r.error.includes('status');"},{"id":"rejects-types","test":"const a = parseOrder({ id: 1042, status: 'shipped', totalCents: '189.00', couponCode: null }); const b = parseOrder({ id: 1042, status: 'shipped', totalCents: 1, couponCode: 5 }); const c = parseOrder([1, 2]); const d = parseOrder(null); return !a.ok && a.error.includes('totalCents') && !b.ok && b.error.includes('couponCode') && !c.ok && !d.ok;"},{"id":"only-known-fields","test":"const r = parseOrder({ id: 1042, status: 'shipped', totalCents: 1, couponCode: null, cardNumber: '4242' }); return r.ok === true && !('cardNumber' in r.value);"},{"id":"asserts","test":"try { assertIsOrder({ id: 'x' }); return false; } catch (e) { return String(e.message).includes('Invalid order'); }"}],"title":"حلّل الطلب عند الحدود","prompt":"<p>استبدل التحويل الكاذب في <code dir=\"ltr\">parseOrder</code> بمحلّل حقيقي، وأعطِ الدوال المساعدة أنواعًا حقيقية:</p>\n<ol>\n<li>تصبح <code dir=\"ltr\">isRecord</code> و<code dir=\"ltr\">isOrderStatus</code> من نوع type predicates.</li>\n<li>تُعيد <code dir=\"ltr\">parseOrder(input: unknown)</code> القيمة <code dir=\"ltr\">ok(order)</code> فقط حين يكون <code dir=\"ltr\">id</code> عددًا صحيحًا، و<code dir=\"ltr\">status</code> حالة معروفة، و<code dir=\"ltr\">totalCents</code> عددًا صحيحًا، و<code dir=\"ltr\">couponCode</code> نصًا أو <code dir=\"ltr\">null</code>. وإلا فإنها تُعيد <code dir=\"ltr\">err(message)</code>، حيث تبدأ الرسالة باسم الحقل الخاطئ (مثلًا <code dir=\"ltr\">status: …</code>). ويجب ألّا يحتوي الطلب المُعاد إلا هذه الحقول الأربعة.</li>\n<li>تصبح <code dir=\"ltr\">assertIsOrder</code> دالة تأكيد تضيّق وسيطها إلى <code dir=\"ltr\">Order</code> وترمي <code dir=\"ltr\">Error</code> تحتوي رسالته <code dir=\"ltr\">Invalid order</code> حين يفشل التحليل.</li>\n</ol>\n<p>بلا تحويلات <code dir=\"ltr\">as</code> في <code dir=\"ltr\">parseOrder</code>.</p>\n","hints":["الـ predicates: غيّر نوع الإرجاع إلى <code dir=\"ltr\">value is Record&lt;string, unknown&gt;</code> و<code dir=\"ltr\">value is OrderStatus</code>.","في <code dir=\"ltr\">parseOrder</code>، افحص <code dir=\"ltr\">isRecord(input)</code> أولًا، وفكّك الحقول الأربعة، وافحص كلًّا منها بـ <code dir=\"ltr\">typeof</code> أو بالـ predicate الخاص بك، مُعيدًا <code dir=\"ltr\">err('field: …')</code> عند أول فشل.","ابنِ النتيجة من المتغيّرات المضيَّقة، <code dir=\"ltr\">ok({ id, status, totalCents, couponCode })</code>، كي يتحقّق منها TypeScript وتُسقط الحقول الإضافية. وفي التأكيد، نوع الإرجاع هو <code dir=\"ltr\">asserts input is Order</code>."],"explanation":"<p>كان <code dir=\"ltr\">input as Order</code> القديم يسمح بمرور أي شيء، بما في ذلك حالة لا فرع لها في الواجهة. الآن يُضيَّق كل حقل بفحص عادي، ويُبنى الكائن الحرفي الأخير من المتغيّرات المضيَّقة، فيتحقّق الـ compiler من مطابقته لـ <code dir=\"ltr\">Order</code> دون تحويل، ولا تعبر الحقول غير المتوقَّعة مثل <code dir=\"ltr\">cardNumber</code> الحدود أبدًا. والـ predicates الاثنان صغيران بما يكفي ليكونا صحيحين بوضوح. وتعيد <code dir=\"ltr\">assertIsOrder</code> استخدام المحلّل وتحوّل الفشل إلى خطأ مرمي، وتوقيعها <code dir=\"ltr\">asserts</code> يضيّق متغيّر المستدعي لبقية النطاق.</p>\n","checkLabels":{"types":"الملف يجتاز فحص الأنواع بلا أي خطأ، بما فيها كل اختبارات الأنواع","accepts-valid":"يُقبل الطلب الصالح","rejects-status":"تُرفض الحالة المجهولة برسالة تسمّي <code dir=\"ltr\">status</code>","rejects-types":"تُرفض أنواع الحقول الخاطئة والمصفوفات و<code dir=\"ltr\">null</code> برسائل تسمّي الحقل","only-known-fields":"لا تُنسخ الحقول غير المتوقَّعة إلى الطلب المحلَّل","asserts":"<code dir=\"ltr\">assertIsOrder</code> ترمي ‘Invalid order…’ مع البيانات السيئة"}},"words":661,"readMinutes":4}