{"id":"l-at-2-4","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Design a fetch client typed from a single route map, so each call infers its input and response types from the route name and callers never write a type argument or a cast.","html":"<p>Every API client starts the same way: <code dir=\"ltr\">fetch(url).then((r) =&gt; r.json())</code>, which returns <code dir=\"ltr\">Promise&lt;any&gt;</code>, and a codebase slowly fills with <code dir=\"ltr\">as Order</code> and <code dir=\"ltr\">as Order[]</code> at call sites. Each cast is a small, unreviewed claim about the server. This lesson puts the whole section together to build a client where callers write no casts and no type arguments, and where a wrong route, a missing field or a mistyped id fails to compile.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">One source of truth: the route map</h2>\n<p>Start by writing down every endpoint once, as a type:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'returned'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">totalCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CancelledOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cancelReason</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">items</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">nextCursor</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'GET /orders/:id'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">output</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'GET /orders'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cursor</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">output</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> };</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'POST /orders/:id/cancel'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">reason</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">output</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CancelledOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">};</span></span></code></pre></div>\n<p>Each key names an endpoint, method and path together. Each value says what the endpoint takes and what it returns. Nothing else in the client will mention <code dir=\"ltr\">Order</code> or <code dir=\"ltr\">Page</code> directly; it will all be derived from this table. Adding an endpoint means adding one line here.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">The route name is the only inference site</h2>\n<p>Now the signature. You want the caller to write the route and the input, and get the right response type back:</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  route</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'input'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'output'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]> {</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // implementation below</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'GET /orders/:id'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });        </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Order</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> page</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'GET /orders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });    </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Page&#x3C;Order></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'POST /orders/:id/cancel'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type '{ id: number; }' is not assignable to parameter</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// of type '{ id: number; reason: string; }'.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">//   Property 'reason' is missing in type '{ id: number; }' but required in type …</span></span></code></pre></div>\n<p>Every idea from this section is at work here. <code dir=\"ltr\">R</code> appears three times, linking the route to the input and the output. Its constraint, <code dir=\"ltr\">keyof Routes</code>, is a union of string literals, so the argument is inferred as the literal <code dir=\"ltr\">'GET /orders/:id'</code> rather than <code dir=\"ltr\">string</code>. And <code dir=\"ltr\">R</code> has exactly one inference site, <code dir=\"ltr\">route</code>; <code dir=\"ltr\">input</code> only <em>uses</em> <code dir=\"ltr\">R</code>, through the indexed access <code dir=\"ltr\">Routes[R]['input']</code>. TypeScript infers the route, then checks the input against what that route needs. No <code dir=\"ltr\">NoInfer</code> required, because nothing else could contribute a candidate.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-at-2-4-t1\"><defs><marker id=\"arrow-l-at-2-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-at-2-4-t1\">The call site passes a route string and an input object. The route string is inferred as the literal key R. R indexes the Routes map, which yields the input type used to check the input argument and the output type used for the returned promise.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"20\" width=\"250\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"135\" y=\"47\" text-anchor=\"middle\">'GET /orders/:id'</text>\n  <rect class=\"d-box\" x=\"10\" y=\"186\" width=\"250\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"135\" y=\"213\" text-anchor=\"middle\">{ id: 1042 }</text>\n  <path class=\"d-arrow\" d=\"M260 42 L318 42\" marker-end=\"url(#arrow-l-at-2-4-1)\"/>\n  <rect class=\"d-box-primary\" x=\"322\" y=\"20\" width=\"120\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"382\" y=\"47\" text-anchor=\"middle\">R</text>\n  <path class=\"d-arrow\" d=\"M382 64 L382 98\" marker-end=\"url(#arrow-l-at-2-4-1)\"/>\n  <rect class=\"d-box-accent\" x=\"292\" y=\"102\" width=\"180\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"382\" y=\"135\" text-anchor=\"middle\">Routes[R]</text>\n  <path class=\"d-arrow\" d=\"M292 150 L264 196\" marker-end=\"url(#arrow-l-at-2-4-1)\"/>\n  <text class=\"d-code\" x=\"190\" y=\"170\" text-anchor=\"middle\">['input'] checks</text>\n  <path class=\"d-arrow\" d=\"M472 130 L520 130\" marker-end=\"url(#arrow-l-at-2-4-1)\"/>\n  <rect class=\"d-box-success\" x=\"524\" y=\"102\" width=\"166\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"607\" y=\"128\" text-anchor=\"middle\">['output']</text>\n  <text class=\"d-label-muted\" x=\"607\" y=\"148\" text-anchor=\"middle\">Promise&lt;Order&gt;</text>\n</svg><figcaption>The route key is inferred once; input and output are looked up from it</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">Unsafe inside, safe outside</h2>\n<p>Inside <code dir=\"ltr\">call</code>, <code dir=\"ltr\">input</code> has the type <code dir=\"ltr\">Routes[R]['input']</code> for some unknown <code dir=\"ltr\">R</code>. TypeScript cannot know which fields it has, so building the URL needs a cast. That is fine, as long as it happens in exactly one place:</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Transport</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">method</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">path</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">body</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> unknown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">unknown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> createClient</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">transport</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Transport</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> call</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    route</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'input'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  )</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'output'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">method</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">template</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> route.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">split</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' '</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">    // The route map guarantees every :param has a matching input field.</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> values</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> input </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">unknown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> rest</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">unknown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">values };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> path</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> template.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">replace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/:(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\w</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)/</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">g</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      delete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rest[name];</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> encodeURIComponent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(values[name]));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> transport</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(method, path, method </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'GET'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rest);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> result </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Routes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">][</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'output'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>There are two casts, both commented, both in the one function every request goes through. Callers get a fully checked API, and a reviewer who wants to audit the unsafe parts reads ten lines. Compare that with a codebase where every one of two hundred call sites says <code dir=\"ltr\">as Order</code>.</p>\n<p>Injecting the <code dir=\"ltr\">transport</code> is a design choice worth copying. The real app passes a function that calls <code dir=\"ltr\">fetch</code>; tests pass a fake that records requests and returns canned data. The types do not care which.</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>Believing the output type</span></p>\n<p><code dir=\"ltr\">Promise&lt;Routes[R]['output']&gt;</code> is a promise <em>you</em> make about the server, and the final <code dir=\"ltr\">as</code> is where you make it. If the backend renames <code dir=\"ltr\">status</code> to <code dir=\"ltr\">state</code>, everything still compiles, and the failure shows up as <code dir=\"ltr\">undefined</code> three components away. The types organise your assumptions; they do not verify them. In <a href=\"/lesson/advanced-typescript/l-at-4-3\">Parsing Untrusted Data at the Boundary</a> you will add a parser per route, so the output type is earned at runtime instead of asserted.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Growing the map</h2>\n<p>Real clients have endpoints that take nothing, such as <code dir=\"ltr\">'GET /me'</code>. With this design you give them <code dir=\"ltr\">input: {}</code> and callers write <code dir=\"ltr\">call('GET /me', {})</code>. That empty object is a small wart, and it is a fair trade for now: the alternative is a signature that makes <code dir=\"ltr\">input</code> optional only for some routes, which needs a conditional type. You will have that tool after <a href=\"/lesson/advanced-typescript/l-at-3-3\">Conditional Types, Distribution and infer</a>, and the change will touch only the signature, not a single call site.</p>\n<p>The same goes for query strings versus bodies, headers, or per-route error types. Each is one more property on the route-map values and one more lookup in the signature. The shape of the solution stays the same: describe it once in the table, derive it everywhere else.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">What makes this design good</h2>\n<p>Look at what the caller never has to do: write <code dir=\"ltr\">&lt;Order&gt;</code>, cast, import response types, or remember which endpoints need which fields. The best generic APIs feel like they have no types at all, because every type is inferred from something the caller had to write anyway (here, the route name).</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>Signs your generic API needs redesigning</span></p>\n<p>Callers writing explicit type arguments, callers casting results, and type parameters that appear only once are all symptoms. They usually mean the inference site is missing or in the wrong place. Find the one argument that identifies what the caller wants, and derive everything else from it.</p>\n</aside>\n<p>In the exercise you will build this client against a fake transport. That ends the generics section. Section 3 takes apart the type operators you have been borrowing (<code dir=\"ltr\">keyof</code>, indexed access) and adds mapped, conditional and template literal types, so you can derive even more from a single source of truth.</p>\n","toc":[{"id":"sec-1","text":"One source of truth: the route map","level":2},{"id":"sec-2","text":"The route name is the only inference site","level":2},{"id":"sec-3","text":"Unsafe inside, safe outside","level":2},{"id":"sec-4","text":"Growing the map","level":2},{"id":"sec-5","text":"What makes this design good","level":2}],"takeaways":["Put every endpoint’s input and output in one route-map type, and derive the client’s signatures from it.","Make the route name the only inference site; everything else is looked up from it with indexed access.","Keep unavoidable casts inside the implementation, in one place, so callers get a fully checked API.","A typed client still trusts the server’s response; validate it at the boundary before relying on the declared output type."],"further":[{"title":"Indexed Access Types","url":"https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html"},{"title":"Generic constraints","url":"https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints"},{"title":"Using the Fetch API (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"}],"quiz":[{"q":"<p>Given <code dir=\"ltr\">call&lt;R extends keyof Routes&gt;(route: R, input: Routes[R]['input']): Promise&lt;Routes[R]['output']&gt;</code>, how is <code dir=\"ltr\">R</code> determined for <code dir=\"ltr\">call('GET /orders/:id', { id: 1042 })</code>?</p>\n","options":[{"text":"From the <code dir=\"ltr\">input</code> object, by matching its shape against every route.","why":"<code dir=\"ltr\">input</code> is typed as <code dir=\"ltr\">Routes[R]['input']</code>, which depends on <code dir=\"ltr\">R</code>; it is checked after <code dir=\"ltr\">R</code> is known, not used to find it."},{"text":"From the route string, which is the literal <code dir=\"ltr\">'GET /orders/:id'</code> because the constraint is a union of string literals.","why":"Correct. The literal stays literal thanks to the <code dir=\"ltr\">keyof Routes</code> constraint, and the other types are looked up from it."},{"text":"From the declared return type at the call site.","why":"TypeScript can use a contextual return type in some cases, but here the route argument decides everything."}],"answer":1},{"q":"<p>The implementation of <code dir=\"ltr\">call</code> needs to read path parameters from <code dir=\"ltr\">input</code>, whose type is <code dir=\"ltr\">Routes[R]['input']</code>. What is the most reasonable approach?</p>","options":[{"text":"Make the public signature take <code dir=\"ltr\">input: any</code> so the implementation can read anything.","why":"That throws away the checking callers rely on, which is the whole point of the client."},{"text":"Write one overload per route so each implementation is concrete.","why":"That duplicates the route map and has to be updated by hand for every new endpoint."},{"text":"Keep the generic public signature and treat <code dir=\"ltr\">input</code> as <code dir=\"ltr\">Record&lt;string, unknown&gt;</code> inside, with one commented cast.","why":"Correct. The cast is local, reviewed once, and invisible to callers, who keep full checking."}],"answer":2},{"q":"<p>Your client declares <code dir=\"ltr\">'GET /orders/:id'</code> as returning <code dir=\"ltr\">Order</code>. The server ships a change that renames <code dir=\"ltr\">status</code> to <code dir=\"ltr\">state</code>. What does TypeScript do?</p>","options":[{"text":"Nothing at compile time; the code reads <code dir=\"ltr\">order.status</code>, gets <code dir=\"ltr\">undefined</code> at runtime, and fails somewhere downstream.","why":"Correct. The route map is a promise about the server that TypeScript cannot check. Validate responses at the boundary to catch this early."},{"text":"It reports an error on the <code dir=\"ltr\">call</code> line the next time you build.","why":"TypeScript never sees the server’s response. It only checks your code against the types you declared."},{"text":"It throws a runtime type error inside <code dir=\"ltr\">call</code>.","why":"Types are erased; nothing checks the response at runtime unless you write that check."}],"answer":0},{"q":"<p>Why is it better to type the route as one string like <code dir=\"ltr\">'POST /orders/:id/cancel'</code> instead of passing method and path as two separate arguments?</p>","options":[{"text":"Because two arguments cannot both be literal types.","why":"They can; each argument can have its own literal type. The issue is keeping them consistent."},{"text":"Because one key gives a single inference site that identifies the endpoint, so method and path can never be mismatched.","why":"Correct. With separate arguments you would need extra machinery to stop a <code dir=\"ltr\">POST</code> being paired with a GET-only path."},{"text":"Because <code dir=\"ltr\">fetch</code> requires the method in the URL.","why":"<code dir=\"ltr\">fetch</code> takes the method in its options object. The single key is a typing choice, not a runtime requirement."}],"answer":1}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype Order = { id: number; status: OrderStatus; totalCents: number };\ntype CancelledOrder = { id: number; status: 'cancelled'; cancelReason: string };\ntype Page<T> = { items: T[]; nextCursor: string | null };\n\n// A fake server: records every request and returns canned data.\nconst sent: string[] = [];\nasync function transport(method: string, path: string, body: unknown): Promise<unknown> {\n  sent.push(`${method} ${path}${body === undefined ? '' : ' ' + JSON.stringify(body)}`);\n  if (path.endsWith('/cancel')) return { id: 1042, status: 'cancelled', cancelReason: 'Changed mind' };\n  if (path === '/orders') return { items: [], nextCursor: null };\n  return { id: 1042, status: 'processing', totalCents: 18900 };\n}\n\ntype Routes = {\n  'GET /orders/:id': { input: { id: number }; output: Order };\n  'GET /orders': { input: { status?: OrderStatus; cursor?: string }; output: Page<Order> };\n  // Add: POST /orders/:id/cancel takes { id, reason } and returns a CancelledOrder\n};\n\nasync function call(route: string, input: Record<string, unknown>): Promise<unknown> {\n  const [method, path] = route.split(' ');\n  // Path parameters like :id are not filled in yet.\n  return transport(method, path, method === 'GET' ? undefined : input);\n}\n\n// ---- Type tests (do not edit) ----\nasync function typeTests() {\n  const order = await call('GET /orders/:id', { id: 1042 });\n  type T1 = Expect<Equal<typeof order, Order>>;\n\n  const page = await call('GET /orders', { status: 'shipped' });\n  type T2 = Expect<Equal<typeof page, Page<Order>>>;\n\n  const cancelled = await call('POST /orders/:id/cancel', { id: 1042, reason: 'Changed mind' });\n  type T3 = Expect<Equal<typeof cancelled, CancelledOrder>>;\n\n  // @ts-expect-error: ids are numbers\n  await call('GET /orders/:id', { id: '1042' });\n  // @ts-expect-error: cancelling needs a reason\n  await call('POST /orders/:id/cancel', { id: 1042 });\n  // @ts-expect-error: there is no such route\n  await call('DELETE /orders/:id', { id: 1042 });\n}\n"},"solution":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype Order = { id: number; status: OrderStatus; totalCents: number };\ntype CancelledOrder = { id: number; status: 'cancelled'; cancelReason: string };\ntype Page<T> = { items: T[]; nextCursor: string | null };\n\n// A fake server: records every request and returns canned data.\nconst sent: string[] = [];\nasync function transport(method: string, path: string, body: unknown): Promise<unknown> {\n  sent.push(`${method} ${path}${body === undefined ? '' : ' ' + JSON.stringify(body)}`);\n  if (path.endsWith('/cancel')) return { id: 1042, status: 'cancelled', cancelReason: 'Changed mind' };\n  if (path === '/orders') return { items: [], nextCursor: null };\n  return { id: 1042, status: 'processing', totalCents: 18900 };\n}\n\ntype Routes = {\n  'GET /orders/:id': { input: { id: number }; output: Order };\n  'GET /orders': { input: { status?: OrderStatus; cursor?: string }; output: Page<Order> };\n  'POST /orders/:id/cancel': { input: { id: number; reason: string }; output: CancelledOrder };\n};\n\nasync function call<R extends keyof Routes>(\n  route: R,\n  input: Routes[R]['input'],\n): Promise<Routes[R]['output']> {\n  const [method, template] = route.split(' ');\n  // The route map guarantees every :param has a matching input field.\n  const values = input as Record<string, unknown>;\n  const rest: Record<string, unknown> = { ...values };\n  const path = template.replace(/:(\\w+)/g, (_, name: string) => {\n    delete rest[name];\n    return encodeURIComponent(String(values[name]));\n  });\n  const result = await transport(method, path, method === 'GET' ? undefined : rest);\n  return result as Routes[R]['output'];\n}\n\n// ---- Type tests (do not edit) ----\nasync function typeTests() {\n  const order = await call('GET /orders/:id', { id: 1042 });\n  type T1 = Expect<Equal<typeof order, Order>>;\n\n  const page = await call('GET /orders', { status: 'shipped' });\n  type T2 = Expect<Equal<typeof page, Page<Order>>>;\n\n  const cancelled = await call('POST /orders/:id/cancel', { id: 1042, reason: 'Changed mind' });\n  type T3 = Expect<Equal<typeof cancelled, CancelledOrder>>;\n\n  // @ts-expect-error: ids are numbers\n  await call('GET /orders/:id', { id: '1042' });\n  // @ts-expect-error: cancelling needs a reason\n  await call('POST /orders/:id/cancel', { id: 1042 });\n  // @ts-expect-error: there is no such route\n  await call('DELETE /orders/:id', { id: 1042 });\n}\n"},"checks":[{"id":"types","typecheck":true},{"id":"fills-params","test":"sent.length = 0; await call('GET /orders/:id', { id: 1042 }); return sent[0] === 'GET /orders/1042';"},{"id":"post-body","test":"sent.length = 0; const r = await call('POST /orders/:id/cancel', { id: 7, reason: 'Late' }); return sent[0] === 'POST /orders/7/cancel {\"reason\":\"Late\"}' && r.status === 'cancelled';"}],"title":"Type the checkout client from a route map","prompt":"<p>Build Cartwheel’s typed <code dir=\"ltr\">call</code> function.</p>\n<ol>\n<li>Add the route <code dir=\"ltr\">'POST /orders/:id/cancel'</code> to <code dir=\"ltr\">Routes</code>: its input is <code dir=\"ltr\">{ id: number; reason: string }</code> and its output is <code dir=\"ltr\">CancelledOrder</code>.</li>\n<li>Make <code dir=\"ltr\">call</code> generic over the route name, so its <code dir=\"ltr\">input</code> and its resolved value are looked up from <code dir=\"ltr\">Routes</code>. The type tests show what callers expect: no type arguments, no casts.</li>\n<li>At runtime, replace each <code dir=\"ltr\">:param</code> in the path with the matching input value, and send the remaining fields as the body for non-GET requests. <code dir=\"ltr\">call('POST /orders/:id/cancel', { id: 7, reason: 'Late' })</code> must send <code dir=\"ltr\">POST /orders/7/cancel</code> with the body <code dir=\"ltr\">{&quot;reason&quot;:&quot;Late&quot;}</code>.</li>\n</ol>\n","hints":["The signature is <code dir=\"ltr\">call&lt;R extends keyof Routes&gt;(route: R, input: Routes[R]['input']): Promise&lt;Routes[R]['output']&gt;</code>.","Inside the function, treat <code dir=\"ltr\">input</code> as <code dir=\"ltr\">Record&lt;string, unknown&gt;</code> with one commented cast, and cast the transport’s result to <code dir=\"ltr\">Routes[R]['output']</code> once.","<code dir=\"ltr\">template.replace(/:(\\w+)/g, (_, name) =&gt; …)</code> gives you each parameter name; copy the input with <code dir=\"ltr\">{ ...values }</code> and <code dir=\"ltr\">delete</code> the names you used, so what is left is the body."],"explanation":"<p><code dir=\"ltr\">R</code> has one inference site, the route string, and its constraint keeps it as a literal key. Every other type is an indexed access on <code dir=\"ltr\">Routes</code>, so adding an endpoint is a one-line change to the map and every caller is checked against it. The two casts live inside <code dir=\"ltr\">call</code>, documented, instead of at every call site. Note that the output type is still a claim about the server: the fake transport returns what the map promises, but a real one could not be trusted without validation.</p>\n","checkLabels":{"types":"The file type-checks with zero errors, including all type tests","fills-params":"Path parameters are filled in: <code dir=\"ltr\">GET /orders/1042</code>","post-body":"The cancel route posts to <code dir=\"ltr\">/orders/7/cancel</code> with only the remaining fields as the body"}},"words":767,"readMinutes":4}