{"id":"l-at-3-4","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Build string types from other types with template literals, transform them with Uppercase and Capitalize, and parse route patterns with infer to type path parameters.","html":"<p>Cartwheel’s checkout emits events like <code dir=\"ltr\">order.shipped</code> and <code dir=\"ltr\">payment.failed</code>, and its API client builds URLs from patterns like <code dir=\"ltr\">/orders/:orderId/lines/:lineId</code>. Both are strings with structure. Typing them as <code dir=\"ltr\">string</code> means a typo in an event name or a missing path parameter only shows up when a listener never fires or a request 404s. Template literal types let the compiler see the structure.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Building strings from types</h2>\n<p>A template literal type uses the same backtick syntax as JavaScript template strings, but with types in the placeholders:</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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `order.${</span><span style=\"color:#953800;--shiki-dark:#FFA657\">OrderStatus</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\">// 'order.processing' | 'order.shipped' | 'order.delivered' | 'order.cancelled' | 'order.returned'</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\"> CheckoutEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `payment.${'captured'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'failed'}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n<p>When a placeholder holds a union, the result has one member per union member. With two placeholders, you get every combination: <code dir=\"ltr\">`${'s' | 'm' | 'l'}-${'red' | 'blue'}`</code> is six strings. This is the feature’s power and its danger, as you will see at the end of the lesson.</p>\n<p>Now an event bus can type its event names from the domain types you already have:</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\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">handler</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:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'order.shipped'</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:#8250DF;--shiki-dark:#D2A8FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'order.shiped'</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:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type '\"order.shiped\"' is not assignable to parameter of type 'CheckoutEvent'.</span></span></code></pre></div>\n<p>Add a status to <code dir=\"ltr\">OrderStatus</code> and <code dir=\"ltr\">order.on-hold</code> becomes a valid event name automatically.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Transforming strings</h2>\n<p>TypeScript ships four <strong>intrinsic</strong> string types that transform literal types: <code dir=\"ltr\">Uppercase</code>, <code dir=\"ltr\">Lowercase</code>, <code dir=\"ltr\">Capitalize</code> and <code dir=\"ltr\">Uncapitalize</code>. You used <code dir=\"ltr\">Capitalize</code> for setter names in <a href=\"/lesson/advanced-typescript/l-at-3-2\">Mapped Types and Key Remapping</a>. Another common use is environment variables and feature flags:</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\"> Feature</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'express-shipping'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'gift-wrap'</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\"> FlagEnvVar</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `CHECKOUT_${</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Uppercase</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Feature</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:#6E7781;--shiki-dark:#8B949E\">// 'CHECKOUT_EXPRESS-SHIPPING' | 'CHECKOUT_GIFT-WRAP'</span></span></code></pre></div>\n<p>They are built into the compiler rather than defined in TypeScript, so you cannot see their source, but they behave like any other generic type.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Parsing strings with infer</h2>\n<p>Template literal types also work as <strong>patterns</strong> in a conditional type. Combined with <code dir=\"ltr\">infer</code>, they pull substrings out of a string literal type:</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Method</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">R</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> R</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> M</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} ${</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> M</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</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\"> M1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Method</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'POST /orders/:id/cancel'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 'POST'</span></span></code></pre></div>\n<p>The pattern <code dir=\"ltr\">`${infer M} ${string}`</code> means “some text, a space, then anything”. TypeScript matches it against the literal and binds <code dir=\"ltr\">M</code> to the part before the first space.</p>\n<p>Here is the type that makes the URL builder safe. It collects every <code dir=\"ltr\">:param</code> in a route pattern:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"4\"><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\"> PathParams</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">P</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</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>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  P</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}:${</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Param</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}/${</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Rest</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Param</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PathParams</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Rest</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    :</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> P</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}:${</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Param</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Param</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</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\"> P1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PathParams</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/orders/:orderId/lines/:lineId'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 'orderId' | 'lineId'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> P2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PathParams</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/orders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;                        </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// never</span></span></code></pre></div>\n<p>Read it as two cases. If there is a parameter followed by more path, capture the parameter and recurse on the rest. If there is a parameter at the end, capture it. Otherwise there are no parameters. When TypeScript matches a pattern, each <code dir=\"ltr\">infer</code> placeholder takes the shortest text that lets the rest of the pattern match, so <code dir=\"ltr\">Param</code> stops at the first <code dir=\"ltr\">/</code>.</p>\n<p>The recursion (<code dir=\"ltr\">PathParams&lt;Rest&gt;</code>) is the subject of the next lesson. For now, see what it gives you:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"5\"><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\"> buildPath</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">P</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</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\">pattern</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> P</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">params</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:#953800;--shiki-dark:#FFA657\">PathParams</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">P</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // Every :param in the pattern has a key in params, by construction.</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\"> params </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\">string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pattern.</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:#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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">buildPath</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/orders/:orderId/lines/:lineId'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { orderId: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, lineId: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// '/orders/1042/lines/3'</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">buildPath</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/orders/:orderId/lines/:lineId'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { orderId: </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: … Property 'lineId' is missing in type '{ orderId: number; }' …</span></span></code></pre></div>\n<p><code dir=\"ltr\">P</code> is inferred as the literal pattern, <code dir=\"ltr\">PathParams&lt;P&gt;</code> turns it into the required keys, and <code dir=\"ltr\">Record</code> turns those into the shape of <code dir=\"ltr\">params</code>. A route with no parameters takes <code dir=\"ltr\">{}</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Template literals as keys</h2>\n<p>Event names are only half the story; each event also carries a payload. A mapped type with an <code dir=\"ltr\">as</code> clause can build the whole name-to-payload table from the status union in one go:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"6\"><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\"> OrderEventPayloads</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:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#953800;--shiki-dark:#FFA657\">S</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `order.${</span><span style=\"color:#953800;--shiki-dark:#FFA657\">S</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</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\">orderId</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\"> S</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ShippedPayload</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderEventPayloads</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'order.shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// { orderId: number; status: 'shipped' }</span></span></code></pre></div>\n<p>The loop runs over statuses, the <code dir=\"ltr\">as</code> clause turns each one into an event name, and the payload type can still refer to the original <code dir=\"ltr\">S</code>. This table is exactly what a typed event emitter needs, and you will build one on top of it in <a href=\"/lesson/advanced-typescript/l-at-4-4\">Type-Safe Event Emitters and Builders</a>. Notice that nothing here is new: it is a mapped type, a template literal and an indexed access, combined. Most real type-level code is a few small pieces like these, put together carefully.</p>\n<aside class=\"callout callout-note\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Loose placeholders</span></p>\n<p><code dir=\"ltr\">${string}</code> and <code dir=\"ltr\">${number}</code> placeholders match broadly. <code dir=\"ltr\">`${number}`</code> accepts <code dir=\"ltr\">'12.50'</code>, but also <code dir=\"ltr\">'1e5'</code> and <code dir=\"ltr\">'-0'</code>, because it means “anything JavaScript reads back as a number”. Types like <code dir=\"ltr\">`${Category}-${string}`</code> for SKUs are useful documentation and catch obvious mistakes, but they are not validation; check real input at runtime.</p>\n</aside>\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>Multiplying unions until the compiler gives up</span></p>\n<p>Each placeholder multiplies the union size. Four placeholders with 40, 30, 15 and 500 members would be nine million strings, and TypeScript stops at 100,000 with <em>Expression produces a union type that is too complex to represent</em>. Long before that limit, editors get slow. Use template literals with small, closed unions (statuses, methods, a handful of locales) and use <code dir=\"ltr\">string</code> for anything high-cardinality, like SKUs or ids.</p>\n</aside>\n<p>In the exercise you will type Cartwheel’s event names and URL builder. Next lesson looks at recursion in types: what it is good for, how deep it can go, and when a simpler type is the better engineering choice.</p>\n","toc":[{"id":"sec-1","text":"Building strings from types","level":2},{"id":"sec-2","text":"Transforming strings","level":2},{"id":"sec-3","text":"Parsing strings with infer","level":2},{"id":"sec-4","text":"Template literals as keys","level":2}],"takeaways":["A template literal type like <code dir=\"ltr\">`order.${OrderStatus}`</code> builds string literal types, and a union inside it produces every combination.","<code dir=\"ltr\">Uppercase</code>, <code dir=\"ltr\">Lowercase</code>, <code dir=\"ltr\">Capitalize</code> and <code dir=\"ltr\">Uncapitalize</code> are built-in types that transform string literal types.","<code dir=\"ltr\">infer</code> inside a template literal pattern captures substrings, which lets you parse route patterns such as <code dir=\"ltr\">/orders/:orderId</code> at the type level.","<code dir=\"ltr\">${number}</code> and <code dir=\"ltr\">${string}</code> placeholders accept loose patterns; they document a format but do not validate it strictly.","Unions inside template literals multiply, and TypeScript refuses unions over 100,000 members, so keep the inputs small."],"further":[{"title":"Template Literal Types","url":"https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html"},{"title":"Intrinsic String Manipulation Types","url":"https://www.typescriptlang.org/docs/handbook/utility-types.html#intrinsic-string-manipulation-types"}],"quiz":[{"q":"<p>How many members does <code dir=\"ltr\">`${'s' | 'm' | 'l'}-${'red' | 'blue'}`</code> have?</p>","options":[{"text":"5, one per literal","why":"Unions in a template literal combine, they do not add. Every size is paired with every colour."},{"text":"6, every size paired with every colour","why":"Correct. The result is the cross product, <code dir=\"ltr\">'s-red' | 's-blue' | 'm-red' | …</code>, so the sizes multiply."},{"text":"1, the pattern string itself","why":"Template literal types with literal unions are expanded into concrete strings, not kept as one pattern."}],"answer":1},{"q":"<p>What does <code dir=\"ltr\">PathParams&lt;'/orders/:orderId/lines/:lineId'&gt;</code> give with the recursive definition from the lesson?</p>","options":[{"text":"<code dir=\"ltr\">'orderId' | 'lineId'</code>","why":"Correct. The first match captures <code dir=\"ltr\">orderId</code> and recurses on <code dir=\"ltr\">lines/:lineId</code>, whose final match captures <code dir=\"ltr\">lineId</code>."},{"text":"<code dir=\"ltr\">':orderId' | ':lineId'</code>","why":"The colon is part of the pattern, outside the <code dir=\"ltr\">infer</code> placeholder, so it is not captured."},{"text":"<code dir=\"ltr\">'orderId'</code> only","why":"That is what you get without the recursive call on <code dir=\"ltr\">Rest</code>; the recursion is what reaches the second parameter."},{"text":"<code dir=\"ltr\">string</code>","why":"<code dir=\"ltr\">infer</code> captures the exact literal substrings, so the result is a union of literals."}],"answer":0},{"q":"<p>Which value is accepted by <code dir=\"ltr\">type Price = </code>${number}``?</p>","options":[{"text":"Only strings like <code dir=\"ltr\">'12.50'</code> that look like money","why":"<code dir=\"ltr\">${number}</code> has no idea about money formats; it accepts anything JavaScript would read back as a number."},{"text":"<code dir=\"ltr\">'1e5'</code> as well as <code dir=\"ltr\">'12.50'</code>","why":"Correct. Any string that JavaScript parses as a number fits, including exponent notation (and even <code dir=\"ltr\">'0x1F'</code>), so it is a loose check."},{"text":"Only integers, because <code dir=\"ltr\">number</code> is not allowed in template literals","why":"<code dir=\"ltr\">number</code> placeholders are allowed and accept decimals too."}],"answer":1},{"q":"<p>A teammate builds <code dir=\"ltr\">`${Locale}/${Currency}/${Category}/${Sku}`</code> from four unions with 40, 30, 15 and 500 members. What happens?</p>","options":[{"text":"It works, but autocompletion becomes slower.","why":"The product is 9 million members, far past the limit; it does not merely slow down."},{"text":"TypeScript keeps it as an unexpanded pattern.","why":"Literal unions are always expanded; there is no lazy mode for them."},{"text":"TypeScript reports “Expression produces a union type that is too complex to represent”.","why":"Correct. The cross product exceeds the 100,000-member limit. Use <code dir=\"ltr\">string</code> for the high-cardinality part, or validate at runtime."}],"answer":2}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype PaymentOutcome = 'captured' | 'failed';\ntype Feature = 'express-shipping' | 'gift-wrap';\n\ntype CheckoutEvent = string;\ntype FlagEnvVar = string;\ntype PathParams<P extends string> = string;\n\nfunction buildPath<P extends string>(pattern: P, params: Record<PathParams<P>, string | number>): string {\n  return pattern;\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<CheckoutEvent,\n  | 'order.processing' | 'order.shipped' | 'order.delivered' | 'order.cancelled' | 'order.returned'\n  | 'payment.captured' | 'payment.failed'>>;\ntype T2 = Expect<Equal<FlagEnvVar, 'CHECKOUT_EXPRESS-SHIPPING' | 'CHECKOUT_GIFT-WRAP'>>;\ntype T3 = Expect<Equal<PathParams<'/orders/:orderId/lines/:lineId'>, 'orderId' | 'lineId'>>;\ntype T4 = Expect<Equal<PathParams<'/customers/:customerId'>, 'customerId'>>;\ntype T5 = Expect<Equal<PathParams<'/orders'>, never>>;\n\n// @ts-expect-error: 'order.shiped' is a typo\nconst typo: CheckoutEvent = 'order.shiped';\n\n// @ts-expect-error: lineId is missing\nbuildPath('/orders/:orderId/lines/:lineId', { orderId: 1042 });\n\nconsole.log(buildPath('/orders/:orderId/lines/:lineId', { orderId: 1042, lineId: 3 }));\n"},"solution":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype PaymentOutcome = 'captured' | 'failed';\ntype Feature = 'express-shipping' | 'gift-wrap';\n\ntype CheckoutEvent = `order.${OrderStatus}` | `payment.${PaymentOutcome}`;\ntype FlagEnvVar = `CHECKOUT_${Uppercase<Feature>}`;\ntype PathParams<P extends string> =\n  P extends `${string}:${infer Param}/${infer Rest}`\n    ? Param | PathParams<Rest>\n    : P extends `${string}:${infer Param}`\n      ? Param\n      : never;\n\nfunction buildPath<P extends string>(pattern: P, params: Record<PathParams<P>, string | number>): string {\n  // Every :param in the pattern has a key in params, by construction.\n  const values = params as Record<string, string | number>;\n  return pattern.replace(/:(\\w+)/g, (_, name: string) => encodeURIComponent(String(values[name])));\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<CheckoutEvent,\n  | 'order.processing' | 'order.shipped' | 'order.delivered' | 'order.cancelled' | 'order.returned'\n  | 'payment.captured' | 'payment.failed'>>;\ntype T2 = Expect<Equal<FlagEnvVar, 'CHECKOUT_EXPRESS-SHIPPING' | 'CHECKOUT_GIFT-WRAP'>>;\ntype T3 = Expect<Equal<PathParams<'/orders/:orderId/lines/:lineId'>, 'orderId' | 'lineId'>>;\ntype T4 = Expect<Equal<PathParams<'/customers/:customerId'>, 'customerId'>>;\ntype T5 = Expect<Equal<PathParams<'/orders'>, never>>;\n\n// @ts-expect-error: 'order.shiped' is a typo\nconst typo: CheckoutEvent = 'order.shiped';\n\n// @ts-expect-error: lineId is missing\nbuildPath('/orders/:orderId/lines/:lineId', { orderId: 1042 });\n\nconsole.log(buildPath('/orders/:orderId/lines/:lineId', { orderId: 1042, lineId: 3 }));\n"},"checks":[{"id":"types","typecheck":true},{"id":"builds","test":"return buildPath('/orders/:orderId/lines/:lineId', { orderId: 1042, lineId: 3 }) === '/orders/1042/lines/3' && buildPath('/orders', {}) === '/orders';"},{"id":"encodes","test":"return buildPath('/search/:q', { q: 'tent & tarp' }) === '/search/tent%20%26%20tarp';"}],"title":"Type event names and URL patterns","prompt":"<p>Use template literal types to type Cartwheel’s strings:</p>\n<ul>\n<li><code dir=\"ltr\">CheckoutEvent</code>: <code dir=\"ltr\">order.&lt;status&gt;</code> for every <code dir=\"ltr\">OrderStatus</code>, plus <code dir=\"ltr\">payment.&lt;outcome&gt;</code> for every <code dir=\"ltr\">PaymentOutcome</code>.</li>\n<li><code dir=\"ltr\">FlagEnvVar</code>: <code dir=\"ltr\">CHECKOUT_</code> followed by each <code dir=\"ltr\">Feature</code> in upper case.</li>\n<li><code dir=\"ltr\">PathParams&lt;P&gt;</code>: the names of every <code dir=\"ltr\">:param</code> in a route pattern (<code dir=\"ltr\">never</code> when there are none).</li>\n</ul>\n<p>Then implement <code dir=\"ltr\">buildPath(pattern, params)</code> at runtime: replace each <code dir=\"ltr\">:param</code> with its value from <code dir=\"ltr\">params</code>, URL-encoded with <code dir=\"ltr\">encodeURIComponent</code>.</p>\n","hints":["<code dir=\"ltr\">`order.${OrderStatus}`</code> expands to one string per status. <code dir=\"ltr\">Uppercase&lt;Feature&gt;</code> works inside a placeholder.","<code dir=\"ltr\">PathParams</code> has two cases: a parameter followed by <code dir=\"ltr\">/</code> and more path (capture it and recurse on the rest), or a parameter at the very end.","At runtime, <code dir=\"ltr\">pattern.replace(/:(\\w+)/g, (_, name) =&gt; …)</code> visits each parameter name. Treat <code dir=\"ltr\">params</code> as <code dir=\"ltr\">Record&lt;string, string | number&gt;</code> inside with one commented cast."],"explanation":"<p>A union inside a template literal expands into one string per member, so both event families are derived from the existing unions. <code dir=\"ltr\">Uppercase</code> transforms each feature name. <code dir=\"ltr\">PathParams</code> pattern-matches the route: <code dir=\"ltr\">infer Param</code> captures the text after a colon up to the next <code dir=\"ltr\">/</code>, and the recursive call handles the rest of the path; the final branch catches a parameter at the end. <code dir=\"ltr\">Record&lt;PathParams&lt;P&gt;, …&gt;</code> then makes every parameter a required key of <code dir=\"ltr\">params</code>, so a missing <code dir=\"ltr\">lineId</code> is a compile error rather than a request to <code dir=\"ltr\">/orders/1042/lines/undefined</code>.</p>\n","checkLabels":{"types":"The file type-checks with zero errors, including all type tests","builds":"<code dir=\"ltr\">buildPath</code> fills in every parameter and leaves parameter-free paths alone","encodes":"Parameter values are URL-encoded"}},"words":704,"readMinutes":3}