{"id":"l-at-4-3","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Treat everything from outside your program as unknown, turn it into trusted domain types with parsers, type predicates and assertion functions, and keep the casts out of your core code.","html":"<p>In <a href=\"/lesson/advanced-typescript/l-at-2-4\">Designing a Typed API Client</a>, the client ended with <code dir=\"ltr\">return result as Routes[R]['output']</code>. That cast was a promise about the server. This lesson is about keeping that promise honest.</p>\n<p>Every program has a <strong>boundary</strong>: the places where data comes from somewhere the compiler cannot see. HTTP responses, <code dir=\"ltr\">JSON.parse</code>, URL parameters, <code dir=\"ltr\">localStorage</code>, form fields, messages from other windows, environment variables. Inside the boundary, types describe values your own code created. At the boundary, types are just hopes. The pattern for handling it has a well-known slogan: <strong>parse, don’t validate</strong>. Turn untyped data into typed data once, at the edge, and let the rest of the program trust the types.</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\">On the left, sources outside the program: fetch responses, JSON.parse, URL parameters and localStorage, all typed as unknown. They pass through parsers at the boundary, which either return an error or a typed Order. Inside the boundary, the core checkout code only sees trusted types.</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\">Outside: 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 params</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\">check, then brand</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\">Inside: trusted</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\">no casts, no checks</text>\n</svg><figcaption>Untrusted data becomes typed data at one gate</figcaption></figure>\n<h2 id=\"sec-1\" tabindex=\"-1\">Start from unknown</h2>\n<p><code dir=\"ltr\">JSON.parse</code> returns <code dir=\"ltr\">any</code>, and so does <code dir=\"ltr\">Response.json()</code>. <code dir=\"ltr\">any</code> is contagious: assign it to a variable typed <code dir=\"ltr\">Order</code> and TypeScript believes you. The first step is to refuse that gift:</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=\"Copy code\"><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>Copy</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> is the honest type for data you have not checked. You can pass it around, but you cannot use it until you narrow it. Narrowing is what a parser does.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">A parser, by hand</h2>\n<p>A parser takes <code dir=\"ltr\">unknown</code> and returns either a typed value or a description of what was wrong. With the <code dir=\"ltr\">Result</code> type from the last lesson:</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=\"Copy code\"><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>Copy</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>Look at the final line: there is no cast. Each check narrowed one variable, and by the end TypeScript can verify that the object literal matches <code dir=\"ltr\">Order</code>. If the type gains a field and the parser forgets it, the <code dir=\"ltr\">ok(…)</code> line fails to compile. This is also where you would brand values, calling <code dir=\"ltr\">toOrderId(id)</code> from <a href=\"/lesson/advanced-typescript/l-at-4-1\">Branded Types and Exhaustive Checks</a>.</p>\n<h3 id=\"sec-3\" tabindex=\"-1\">Closing the gap in the API client</h3>\n<p>Back in the client, the fix for that final cast is a table of parsers, one per route, checked against the route map with a mapped type and <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=\"Copy code\"><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>Copy</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>Forget a route and <code dir=\"ltr\">satisfies</code> reports the missing key; return the wrong type from a parser and it reports that too. Inside <code dir=\"ltr\">call</code>, the response now goes through <code dir=\"ltr\">parsers[route]</code> before it is returned. TypeScript cannot connect the generic <code dir=\"ltr\">R</code> to the matching row of the table, so one annotation remains there, but it now sits on data that has actually been checked: the client’s output types are earned instead of asserted.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Type predicates</h2>\n<p><code dir=\"ltr\">isRecord</code> and <code dir=\"ltr\">isOrderStatus</code> are <strong>type predicates</strong>: functions whose return type is <code dir=\"ltr\">value is T</code>. When one returns <code dir=\"ltr\">true</code>, TypeScript narrows the argument at the call site. Since TypeScript 5.5, simple ones like <code dir=\"ltr\">(x) =&gt; x !== null</code> are inferred, as you saw in section 1, but anything with real logic needs the annotation.</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\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Predicates that lie</span></p>\n<p>TypeScript does not check that a predicate’s body proves its claim. <code dir=\"ltr\">function isOrder(v: unknown): v is Order { return isRecord(v) &amp;&amp; 'id' in v; }</code> compiles, and every caller now believes a customer record is an order. A predicate is an <code dir=\"ltr\">as</code> cast with a function around it. Keep predicates small and obviously correct, like <code dir=\"ltr\">isOrderStatus</code>, and build bigger checks from them in parsers that construct the value, so the compiler verifies the result.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Assertion functions</h2>\n<p>Sometimes you want to narrow and stop if the data is bad, without an <code dir=\"ltr\">if</code> at every call site. An <strong>assertion function</strong> returns nothing and throws on failure, and its <code dir=\"ltr\">asserts</code> return type narrows everything after the call:</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=\"Copy code\"><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>Copy</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>Assertion functions fit places where bad data is a bug rather than an expected case: test helpers, data your own server rendered into the page, invariants. For HTTP responses, prefer a parser that returns a <code dir=\"ltr\">Result</code>, so a changed API produces a handled error rather than a crash. One quirk: an assertion function must be called through a name with an explicit type, so <code dir=\"ltr\">const assertIsOrder = (x: unknown): asserts x is Order =&gt; …</code> needs a type annotation on the <code dir=\"ltr\">const</code> itself, or TypeScript reports “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\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Use a schema library in real projects</span></p>\n<p>Hand-written parsers are perfect for learning and fine for a few types. For a whole API, use a schema library such as Zod, Valibot or ArkType: you declare the shape once as a runtime value, and the library both checks data and gives you the TypeScript type derived from it, so the parser and the type cannot drift apart. It is the same <code dir=\"ltr\">typeof</code>-style “single source of truth” idea from section 3, packaged for you.</p>\n</aside>\n<p>In the exercise you will write Cartwheel’s order parser, a predicate and an assertion function, and replace a cast that has been lying since day one. Next lesson builds typed event emitters and builders on top of everything in this section.</p>\n","toc":[{"id":"sec-1","text":"Start from unknown","level":2},{"id":"sec-2","text":"A parser, by hand","level":2},{"id":"sec-3","text":"Closing the gap in the API client","level":3},{"id":"sec-4","text":"Type predicates","level":2},{"id":"sec-5","text":"Assertion functions","level":2}],"takeaways":["<code dir=\"ltr\">JSON.parse</code>, <code dir=\"ltr\">response.json()</code> and <code dir=\"ltr\">localStorage</code> give you <code dir=\"ltr\">any</code>; annotate their results as <code dir=\"ltr\">unknown</code> so the compiler forces a check.","A parser takes <code dir=\"ltr\">unknown</code> and returns a typed value or an error, so the rest of the code only ever sees data that has been checked.","A type predicate (<code dir=\"ltr\">value is T</code>) narrows at the call site, but TypeScript trusts its body, so a wrong predicate is an unchecked cast.","An assertion function (<code dir=\"ltr\">asserts value is T</code>) narrows everything after the call and must throw when the check fails.","Schema libraries declare the check once and derive the type from it, which removes the risk of a parser and a type drifting apart."],"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> What is the type of <code dir=\"ltr\">data</code>, and what should you do about it?</p>","options":[{"text":"<code dir=\"ltr\">unknown</code>; narrow it before use.","why":"<code dir=\"ltr\">Response.json()</code> is declared to return <code dir=\"ltr\">Promise&lt;any&gt;</code>, not <code dir=\"ltr\">unknown</code>, so nothing forces you to narrow unless you annotate it."},{"text":"<code dir=\"ltr\">Order</code>, inferred from the endpoint URL.","why":"TypeScript cannot know what a server returns; the URL is just a string to it."},{"text":"<code dir=\"ltr\">object</code>; cast it with <code dir=\"ltr\">as Order</code>.","why":"The type is <code dir=\"ltr\">any</code>, and a cast skips the check that would catch a changed API."},{"text":"<code dir=\"ltr\">any</code>; annotate it as <code dir=\"ltr\">unknown</code> and run it through a parser.","why":"Correct. <code dir=\"ltr\">any</code> switches checking off for every use; <code dir=\"ltr\">unknown</code> forces each use to be proved first."}],"answer":3},{"q":"<p>What is wrong with this 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=\"Copy code\"><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>Copy</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":"Nothing; checking for <code dir=\"ltr\">id</code> is enough to prove a value is an <code dir=\"ltr\">Order</code>.","why":"A customer record also has <code dir=\"ltr\">id</code>. The predicate claims far more than it checks."},{"text":"It claims the value is a full <code dir=\"ltr\">Order</code> but only checks for <code dir=\"ltr\">id</code>, and TypeScript trusts the claim without verifying it.","why":"Correct. A predicate’s body is not checked against its return type, so a weak check is an unchecked cast."},{"text":"Predicates cannot use the <code dir=\"ltr\">in</code> operator.","why":"<code dir=\"ltr\">in</code> is a normal narrowing check and works fine inside predicates."}],"answer":1},{"q":"<p>How does an assertion function differ from a type predicate?</p>","options":[{"text":"An assertion function returns <code dir=\"ltr\">boolean</code>; a predicate returns <code dir=\"ltr\">void</code>.","why":"It is the other way round. A predicate returns <code dir=\"ltr\">boolean</code>; an assertion function returns nothing and throws on failure."},{"text":"An assertion function narrows the value for the rest of the scope after the call, and throws if the check fails.","why":"Correct. <code dir=\"ltr\">assertIsOrder(x); x.id</code> works, because returning normally proves the assertion."},{"text":"Assertion functions only work on primitives.","why":"They work on any type, including objects and unions."}],"answer":1},{"q":"<p>Where should a value from <code dir=\"ltr\">localStorage.getItem('cart')</code> be parsed?</p>","options":[{"text":"Right where it is read, at the boundary, before any other code sees it.","why":"Correct. Parse once at the edge, and every function inside the app can trust the type."},{"text":"In each component that uses the cart, just before rendering.","why":"Parsing in many places duplicates the logic and leaves windows where unchecked data flows around."},{"text":"Nowhere; your own app wrote it, so its shape is guaranteed.","why":"Old versions of your app, browser extensions and users with devtools all write to storage. Treat it as untrusted."},{"text":"In the reducer that stores the cart, after it has been used for the first render.","why":"Unchecked data has already been rendered by then; the boundary is the first read, not a later step."}],"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":"Parse the order at the boundary","prompt":"<p>Replace the lying cast in <code dir=\"ltr\">parseOrder</code> with a real parser, and give the helpers real types:</p>\n<ol>\n<li><code dir=\"ltr\">isRecord</code> and <code dir=\"ltr\">isOrderStatus</code> become type predicates.</li>\n<li><code dir=\"ltr\">parseOrder(input: unknown)</code> returns <code dir=\"ltr\">ok(order)</code> only when <code dir=\"ltr\">id</code> is an integer, <code dir=\"ltr\">status</code> is a known status, <code dir=\"ltr\">totalCents</code> is a whole number, and <code dir=\"ltr\">couponCode</code> is a string or <code dir=\"ltr\">null</code>. Otherwise it returns <code dir=\"ltr\">err(message)</code>, where the message starts with the name of the bad field (for example <code dir=\"ltr\">status: …</code>). The returned order must contain only those four fields.</li>\n<li><code dir=\"ltr\">assertIsOrder</code> becomes an assertion function that narrows its argument to <code dir=\"ltr\">Order</code> and throws an <code dir=\"ltr\">Error</code> whose message contains <code dir=\"ltr\">Invalid order</code> when parsing fails.</li>\n</ol>\n<p>No <code dir=\"ltr\">as</code> casts in <code dir=\"ltr\">parseOrder</code>.</p>\n","hints":["Predicates: change the return type to <code dir=\"ltr\">value is Record&lt;string, unknown&gt;</code> and <code dir=\"ltr\">value is OrderStatus</code>.","In <code dir=\"ltr\">parseOrder</code>, check <code dir=\"ltr\">isRecord(input)</code> first, destructure the four fields, and check each with <code dir=\"ltr\">typeof</code> or your predicate, returning <code dir=\"ltr\">err('field: …')</code> on the first failure.","Build the result from the narrowed variables, <code dir=\"ltr\">ok({ id, status, totalCents, couponCode })</code>, so TypeScript verifies it and extra fields are dropped. For the assertion, the return type is <code dir=\"ltr\">asserts input is Order</code>."],"explanation":"<p>The old <code dir=\"ltr\">input as Order</code> let anything through, including a status the UI has no branch for. Now each field is narrowed by an ordinary check, and the final object literal is built from the narrowed variables, so the compiler verifies it matches <code dir=\"ltr\">Order</code> without a cast, and unexpected fields such as <code dir=\"ltr\">cardNumber</code> never get past the boundary. The two predicates are small enough to be obviously correct. <code dir=\"ltr\">assertIsOrder</code> reuses the parser and turns a failure into a thrown error, and its <code dir=\"ltr\">asserts</code> signature narrows the caller’s variable for the rest of the scope.</p>\n","checkLabels":{"types":"The file type-checks with zero errors, including all type tests","accepts-valid":"A valid order is accepted","rejects-status":"An unknown status is rejected with a message naming <code dir=\"ltr\">status</code>","rejects-types":"Wrong field types, arrays and <code dir=\"ltr\">null</code> are rejected with messages naming the field","only-known-fields":"Unexpected fields are not copied into the parsed order","asserts":"<code dir=\"ltr\">assertIsOrder</code> throws ‘Invalid order…’ for bad data"}},"words":777,"readMinutes":4}