{"id":"l-at-3-3","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Write conditional types that choose between types, predict how they distribute over unions, and pull types out of other types with infer, re-implementing Exclude, Extract and ReturnType.","html":"<p>Back in <a href=\"/lesson/advanced-typescript/l-at-2-4\">Designing a Typed API Client</a>, routes with no input forced callers to write <code dir=\"ltr\">call('GET /me', {})</code>. The fix needs a type that <em>makes a decision</em>: “if this route’s input has no required fields, make the argument optional; otherwise, require it”. Mapped types loop. Conditional types decide.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The type-level ternary</h2>\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\"> IsArray</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:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> readonly</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:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</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\"> A</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> IsArray</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:#6E7781;--shiki-dark:#8B949E\">// true</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> B</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> IsArray</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:#6E7781;--shiki-dark:#8B949E\">// false</span></span></code></pre></div>\n<p><code dir=\"ltr\">T extends U ? X : Y</code> reads like a ternary: if <code dir=\"ltr\">T</code> is assignable to <code dir=\"ltr\">U</code>, the result is <code dir=\"ltr\">X</code>, otherwise <code dir=\"ltr\">Y</code>. The <code dir=\"ltr\">extends</code> here is the same assignability check as everywhere else in TypeScript, “is <code dir=\"ltr\">T</code> a subset of <code dir=\"ltr\">U</code>?”. When <code dir=\"ltr\">T</code> is a generic parameter that is not known yet, TypeScript <strong>defers</strong> the conditional and keeps it as is until a concrete type arrives.</p>\n<p>Here is the API client fix. <code dir=\"ltr\">{} extends Input</code> is true exactly when every property of <code dir=\"ltr\">Input</code> is optional (an empty object would satisfy it):</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CallArgs</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {} </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">extends</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:#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\">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:#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:#1F2328;--shiki-dark:#E6EDF3\">: </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>\n<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\"> 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 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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">args</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CallArgs</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\"> 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>\n<span class=\"line\"><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 /me'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);                    </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// ok: input is {}</span></span>\n<span class=\"line\"><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\">);                </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// ok: every filter is optional</span></span>\n<span class=\"line\"><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\">);            </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Expected 2 arguments, but got 1.</span></span>\n<span class=\"line\"><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\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// ok</span></span></code></pre></div>\n<p>The conditional produces a <em>tuple</em> type, and the rest parameter <code dir=\"ltr\">...args</code> spreads it into the parameter list. Labeled tuple elements (<code dir=\"ltr\">input?:</code>) keep the parameter name in tooltips. Not one call site changed.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Distribution over unions</h2>\n<p>Conditional types have one behaviour that surprises everyone. When the checked type is a <strong>naked type parameter</strong> and you pass it a union, the conditional runs once per member and the results are joined:</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\"> ToArray</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:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> unknown</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:#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\"> X</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ToArray</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</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:#6E7781;--shiki-dark:#8B949E\">// string[] | number[], not (string | number)[]</span></span></code></pre></div>\n<p>This is called <strong>distribution</strong>, and it is the engine behind several utility types. Here are <code dir=\"ltr\">Exclude</code> and <code dir=\"ltr\">Extract</code>, exactly as the lib defines them:</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\"> MyExclude</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:#953800;--shiki-dark:#FFA657\">U</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> U</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyExtract</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:#953800;--shiki-dark:#FFA657\">U</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> U</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</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\"> Active</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyExclude</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">OrderStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </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 style=\"color:#6E7781;--shiki-dark:#8B949E\">// 'processing' | 'shipped' | 'delivered'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Shipped</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyExtract</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:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</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\">// the ShippedOrder variant</span></span></code></pre></div>\n<p>Each member goes through the conditional on its own. Members mapped to <code dir=\"ltr\">never</code> vanish, because <code dir=\"ltr\">never</code> is the empty union. <code dir=\"ltr\">MyExtract&lt;Order, { status: 'shipped' }&gt;</code> is the cleanest way to name one variant of a discriminated union.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 240\" role=\"img\" aria-labelledby=\"l-at-3-3-t1\"><defs><marker id=\"arrow-l-at-3-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-3-3-t1\">The union of processing, shipped and cancelled enters Exclude with cancelled. It is split into three members. Each member is tested against cancelled: processing and shipped are kept, cancelled becomes never. The results are joined into processing or shipped.</title>\n  <rect class=\"d-box-accent\" x=\"10\" y=\"96\" width=\"150\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"85\" y=\"125\" text-anchor=\"middle\">A | B | C</text>\n  <path class=\"d-arrow\" d=\"M160 110 L224 50\" marker-end=\"url(#arrow-l-at-3-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M160 120 L224 120\" marker-end=\"url(#arrow-l-at-3-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M160 130 L224 190\" marker-end=\"url(#arrow-l-at-3-3-1)\"/>\n  <rect class=\"d-box\" x=\"228\" y=\"28\" width=\"230\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"343\" y=\"55\" text-anchor=\"middle\">'processing' ext C? → keep</text>\n  <rect class=\"d-box\" x=\"228\" y=\"98\" width=\"230\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"343\" y=\"125\" text-anchor=\"middle\">'shipped' ext C? → keep</text>\n  <rect class=\"d-box-warn\" x=\"228\" y=\"168\" width=\"230\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"343\" y=\"195\" text-anchor=\"middle\">'cancelled' ext C? → never</text>\n  <path class=\"d-arrow\" d=\"M458 50 L522 110\" marker-end=\"url(#arrow-l-at-3-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M458 120 L522 120\" marker-end=\"url(#arrow-l-at-3-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M458 190 L522 130\" marker-end=\"url(#arrow-l-at-3-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"526\" y=\"96\" width=\"164\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"608\" y=\"125\" text-anchor=\"middle\">'processing' | 'shipped'</text>\n</svg><figcaption>A distributive conditional splits the union, runs per member, and re-joins</figcaption></figure>\n<p>To turn distribution off, wrap both sides in a one-element tuple. <code dir=\"ltr\">[T]</code> is not a naked type parameter, so the union is checked as a whole:</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\"> ToArrayWhole</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\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">extends</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:#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:#0550AE;--shiki-dark:#79C0FF\"> never</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\"> Y</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ToArrayWhole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</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:#6E7781;--shiki-dark:#8B949E\">// (string | number)[]</span></span></code></pre></div>\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>Forgetting that never is an empty union</span></p>\n<p>A distributive conditional given <code dir=\"ltr\">never</code> returns <code dir=\"ltr\">never</code>, without evaluating either branch, because there are no members to run it on. So <code dir=\"ltr\">IsString&lt;never&gt;</code> is <code dir=\"ltr\">never</code>, not <code dir=\"ltr\">true</code> or <code dir=\"ltr\">false</code>. If you write type tests for a conditional type, include <code dir=\"ltr\">never</code> and a union among the inputs, and use <code dir=\"ltr\">[T] extends [never]</code> when you need to detect <code dir=\"ltr\">never</code> itself.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">infer: capture part of a type</h2>\n<p>Inside the <code dir=\"ltr\">extends</code> clause, <code dir=\"ltr\">infer X</code> declares a type variable that captures whatever sits in that position, if the pattern matches. This is essentially how the lib defines <code dir=\"ltr\">ReturnType</code> (the lib version also constrains <code dir=\"ltr\">F</code> to a function type and falls back to <code dir=\"ltr\">any</code>):</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyReturnType</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">F</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> F</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">args</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> any</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> R</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\"> :</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\"> Loaded</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyReturnType</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> loadOrder>; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Promise&#x3C;Order></span></span></code></pre></div>\n<p>The same trick pulls types out of anything with structure:</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\"> ElementOf</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:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> readonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> UnwrapPromise</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:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> V</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> V</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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> FirstArg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">F</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> F</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">first</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> A</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">rest</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> any</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> any</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> A</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></code></pre></div>\n<p><code dir=\"ltr\">infer</code> can also carry a constraint, as in <code dir=\"ltr\">infer S extends string</code>, which only matches when the captured type is a string and gives you <code dir=\"ltr\">S</code> already narrowed. You will need that in the next lesson, when you capture pieces of string literal types.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Filtering keys by value type</h2>\n<p>Combine a mapped type, a conditional and an indexed access, and you can select keys by what their values are:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"7\"><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\"> KeysOfType</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:#953800;--shiki-dark:#FFA657\">V</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\">K</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\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</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\">K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> V</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</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\"> Line</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">sku</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\">quantity</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\">unitPriceCents</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\"> NumericField</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> KeysOfType</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Line</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 style=\"color:#6E7781;--shiki-dark:#8B949E\">// 'quantity' | 'unitPriceCents'</span></span></code></pre></div>\n<p>The mapped type turns each property into its own key name or <code dir=\"ltr\">never</code>, and <code dir=\"ltr\">[keyof T]</code> collects the values into a union, where the <code dir=\"ltr\">never</code>s disappear. It reads backwards the first time; write it once, name it well, and reuse it. A sort function typed <code dir=\"ltr\">sortBy(lines, key: KeysOfType&lt;Line, number&gt;)</code> now refuses to sort by <code dir=\"ltr\">sku</code>.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Conditional return types: handle with care</h2>\n<p>It is tempting to give a function a conditional return type, such as <code dir=\"ltr\">function format&lt;T extends number | number[]&gt;(x: T): T extends number ? string : string[]</code>. Callers get precise types, but the implementation does not: inside the function, <code dir=\"ltr\">T</code> is still unknown, the conditional stays deferred, and TypeScript cannot check that <code dir=\"ltr\">return x.toFixed(2)</code> matches it. You end up writing <code dir=\"ltr\">as any</code> or <code dir=\"ltr\">as T extends number ? string : string[]</code> on every return, which means the most delicate code in the function is the code nobody checks.</p>\n<p>That is why the previous section preferred overloads or separate functions for this job. Conditional types shine in <em>derived</em> types (<code dir=\"ltr\">CallArgs</code>, <code dir=\"ltr\">Extract</code>, <code dir=\"ltr\">KeysOfType</code>) where nothing has to be implemented against them, and they are weakest as the declared return type of hand-written logic.</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>Test conditional types like functions</span></p>\n<p>Conditional types are small programs with edge cases. Test them the way the exercises do: a normal case, a union, <code dir=\"ltr\">never</code>, <code dir=\"ltr\">any</code> and an optional property. Five <code dir=\"ltr\">Expect&lt;Equal&lt;…&gt;&gt;</code> lines catch most surprises before your teammates do.</p>\n</aside>\n<p>In the exercise you will re-implement <code dir=\"ltr\">Exclude</code>, <code dir=\"ltr\">Extract</code> and <code dir=\"ltr\">ReturnType</code> and use them on Cartwheel’s order types. The next lesson applies conditionals and <code dir=\"ltr\">infer</code> to strings.</p>\n","toc":[{"id":"sec-1","text":"The type-level ternary","level":2},{"id":"sec-2","text":"Distribution over unions","level":2},{"id":"sec-3","text":"infer: capture part of a type","level":2},{"id":"sec-4","text":"Filtering keys by value type","level":2},{"id":"sec-5","text":"Conditional return types: handle with care","level":2}],"takeaways":["<code dir=\"ltr\">T extends U ? X : Y</code> picks <code dir=\"ltr\">X</code> when <code dir=\"ltr\">T</code> is assignable to <code dir=\"ltr\">U</code> and <code dir=\"ltr\">Y</code> otherwise; with a generic <code dir=\"ltr\">T</code>, the choice waits until <code dir=\"ltr\">T</code> is known.","A conditional on a naked type parameter distributes over unions, running once per member and joining the results.","Wrap both sides in brackets, <code dir=\"ltr\">[T] extends [U]</code>, to test the union as a whole instead of distributing.","<code dir=\"ltr\">infer</code> declares a type variable inside the <code dir=\"ltr\">extends</code> clause and captures whatever matches it, which is how <code dir=\"ltr\">ReturnType</code> and <code dir=\"ltr\">Awaited</code> work.","Mapping each key to itself or <code dir=\"ltr\">never</code> and then indexing with <code dir=\"ltr\">[keyof T]</code> filters keys by the type of their value."],"further":[{"title":"Conditional Types","url":"https://www.typescriptlang.org/docs/handbook/2/conditional-types.html"},{"title":"Utility Types (Exclude, Extract, ReturnType)","url":"https://www.typescriptlang.org/docs/handbook/utility-types.html"}],"quiz":[{"q":"<p><code dir=\"ltr\">type ToArray&lt;T&gt; = T extends unknown ? T[] : never</code>. What is <code dir=\"ltr\">ToArray&lt;string | number&gt;</code>?</p>","options":[{"text":"<code dir=\"ltr\">(string | number)[]</code>","why":"That is the non-distributive result, which you get with <code dir=\"ltr\">[T] extends [unknown]</code>."},{"text":"<code dir=\"ltr\">never</code>","why":"Both members extend <code dir=\"ltr\">unknown</code>, so neither takes the <code dir=\"ltr\">never</code> branch."},{"text":"<code dir=\"ltr\">unknown[]</code>","why":"The true branch uses <code dir=\"ltr\">T</code>, which is each member in turn, not the constraint <code dir=\"ltr\">unknown</code>."},{"text":"<code dir=\"ltr\">string[] | number[]</code>","why":"Correct. <code dir=\"ltr\">T</code> is a naked type parameter, so the conditional runs once for <code dir=\"ltr\">string</code> and once for <code dir=\"ltr\">number</code>, and the results are joined."}],"answer":3},{"q":"<p>Which type gives you the shipped variant of the <code dir=\"ltr\">Order</code> union?</p>","options":[{"text":"<code dir=\"ltr\">Extract&lt;Order, { status: 'shipped' }&gt;</code>","why":"Correct. <code dir=\"ltr\">Extract</code> keeps the union members assignable to <code dir=\"ltr\">{ status: 'shipped' }</code>, which is exactly the shipped variant."},{"text":"<code dir=\"ltr\">Exclude&lt;Order, { status: 'shipped' }&gt;</code>","why":"<code dir=\"ltr\">Exclude</code> does the opposite: it removes the shipped variant and keeps the rest."},{"text":"<code dir=\"ltr\">Order['shipped']</code>","why":"Indexed access looks up a property named <code dir=\"ltr\">shipped</code>, which <code dir=\"ltr\">Order</code> does not have."},{"text":"<code dir=\"ltr\">Order extends { status: 'shipped' } ? Order : never</code>","why":"<code dir=\"ltr\">Order</code> is a concrete type here, not a type parameter, so nothing distributes. The whole union is not assignable to the shipped shape, so you get <code dir=\"ltr\">never</code>."}],"answer":0},{"q":"<p>What is <code dir=\"ltr\">R</code>?</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\"> MyReturnType</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">F</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> F</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">args</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> any</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> R</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\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</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\"> R</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyReturnType</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;(</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:#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\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>>;</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">Order</code>","why":"<code dir=\"ltr\">infer R</code> captures the return type as written. Unwrapping the promise needs a second step, such as <code dir=\"ltr\">Awaited</code>."},{"text":"<code dir=\"ltr\">Promise&lt;Order&gt;</code>","why":"Correct. The function type matches the pattern, and <code dir=\"ltr\">R</code> captures its return type exactly."},{"text":"<code dir=\"ltr\">never</code>","why":"You get <code dir=\"ltr\">never</code> only when <code dir=\"ltr\">F</code> is not a function type. Here it matches."}],"answer":1},{"q":"<p><code dir=\"ltr\">type IsString&lt;T&gt; = T extends string ? true : false</code>. What is <code dir=\"ltr\">IsString&lt;never&gt;</code>?</p>","options":[{"text":"<code dir=\"ltr\">true</code>, because <code dir=\"ltr\">never</code> is assignable to everything","why":"That is true for a non-distributive check like <code dir=\"ltr\">[never] extends [string]</code>. A distributive conditional never gets that far."},{"text":"<code dir=\"ltr\">false</code>, because <code dir=\"ltr\">never</code> is not a string","why":"The conditional is not evaluated at all; there are no union members to run it on."},{"text":"<code dir=\"ltr\">never</code>, because distributing over an empty union produces an empty union","why":"Correct. <code dir=\"ltr\">never</code> is the empty union, so a distributive conditional maps zero members and returns <code dir=\"ltr\">never</code>."}],"answer":2}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype Order =\n  | { id: number; status: 'processing' }\n  | { id: number; status: 'shipped'; trackingNumber: string }\n  | { id: number; status: 'delivered'; trackingNumber: string; deliveredAt: string }\n  | { id: number; status: 'cancelled'; cancelReason: string }\n  | { id: number; status: 'returned'; trackingNumber: string; deliveredAt: string; refundCents: number };\ntype Line = { sku: string; quantity: number; unitPriceCents: number; giftWrapped: boolean };\ndeclare function loadOrder(id: number): Promise<Order>;\n\ntype MyExclude<T, U> = T;\ntype MyExtract<T, U> = T;\ntype MyReturnType<F> = unknown;\ntype UnwrapPromise<T> = T;\n\n// The Order variant(s) whose status is S.\ntype OrderWithStatus<S extends OrderStatus> = Order;\n\n// The keys of T whose values are assignable to V.\ntype KeysOfType<T, V> = keyof T;\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<MyExclude<OrderStatus, 'cancelled' | 'returned'>, 'processing' | 'shipped' | 'delivered'>>;\ntype T2 = Expect<Equal<MyExtract<OrderStatus, 'shipped' | 'refunded'>, 'shipped'>>;\ntype T3 = Expect<Equal<MyReturnType<typeof loadOrder>, Promise<Order>>>;\ntype T4 = Expect<Equal<MyReturnType<() => void>, void>>;\ntype T5 = Expect<Equal<UnwrapPromise<Promise<Order>>, Order>>;\ntype T6 = Expect<Equal<UnwrapPromise<number>, number>>;\ntype T7 = Expect<Equal<OrderWithStatus<'cancelled'>, { id: number; status: 'cancelled'; cancelReason: string }>>;\ntype T8 = Expect<Equal<OrderWithStatus<'delivered' | 'returned'>['deliveredAt'], string>>;\ntype T9 = Expect<Equal<KeysOfType<Line, number>, 'quantity' | 'unitPriceCents'>>;\ntype T10 = Expect<Equal<KeysOfType<Line, boolean>, 'giftWrapped'>>;\n\nconst shippedOnly: OrderWithStatus<'shipped'> = { id: 1042, status: 'shipped', trackingNumber: '1Z-999' };\nconsole.log(shippedOnly.trackingNumber);\n"},"solution":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype Order =\n  | { id: number; status: 'processing' }\n  | { id: number; status: 'shipped'; trackingNumber: string }\n  | { id: number; status: 'delivered'; trackingNumber: string; deliveredAt: string }\n  | { id: number; status: 'cancelled'; cancelReason: string }\n  | { id: number; status: 'returned'; trackingNumber: string; deliveredAt: string; refundCents: number };\ntype Line = { sku: string; quantity: number; unitPriceCents: number; giftWrapped: boolean };\ndeclare function loadOrder(id: number): Promise<Order>;\n\ntype MyExclude<T, U> = T extends U ? never : T;\ntype MyExtract<T, U> = T extends U ? T : never;\ntype MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;\ntype UnwrapPromise<T> = T extends Promise<infer V> ? V : T;\n\n// The Order variant(s) whose status is S.\ntype OrderWithStatus<S extends OrderStatus> = MyExtract<Order, { status: S }>;\n\n// The keys of T whose values are assignable to V.\ntype KeysOfType<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<MyExclude<OrderStatus, 'cancelled' | 'returned'>, 'processing' | 'shipped' | 'delivered'>>;\ntype T2 = Expect<Equal<MyExtract<OrderStatus, 'shipped' | 'refunded'>, 'shipped'>>;\ntype T3 = Expect<Equal<MyReturnType<typeof loadOrder>, Promise<Order>>>;\ntype T4 = Expect<Equal<MyReturnType<() => void>, void>>;\ntype T5 = Expect<Equal<UnwrapPromise<Promise<Order>>, Order>>;\ntype T6 = Expect<Equal<UnwrapPromise<number>, number>>;\ntype T7 = Expect<Equal<OrderWithStatus<'cancelled'>, { id: number; status: 'cancelled'; cancelReason: string }>>;\ntype T8 = Expect<Equal<OrderWithStatus<'delivered' | 'returned'>['deliveredAt'], string>>;\ntype T9 = Expect<Equal<KeysOfType<Line, number>, 'quantity' | 'unitPriceCents'>>;\ntype T10 = Expect<Equal<KeysOfType<Line, boolean>, 'giftWrapped'>>;\n\nconst shippedOnly: OrderWithStatus<'shipped'> = { id: 1042, status: 'shipped', trackingNumber: '1Z-999' };\nconsole.log(shippedOnly.trackingNumber);\n"},"checks":[{"id":"types","typecheck":true}],"title":"Build Exclude, Extract and friends","prompt":"<p>Implement these with conditional types (do not use the built-in <code dir=\"ltr\">Exclude</code>, <code dir=\"ltr\">Extract</code>, <code dir=\"ltr\">ReturnType</code> or <code dir=\"ltr\">Awaited</code>):</p>\n<ul>\n<li><code dir=\"ltr\">MyExclude&lt;T, U&gt;</code>: the members of <code dir=\"ltr\">T</code> not assignable to <code dir=\"ltr\">U</code>.</li>\n<li><code dir=\"ltr\">MyExtract&lt;T, U&gt;</code>: the members of <code dir=\"ltr\">T</code> assignable to <code dir=\"ltr\">U</code>.</li>\n<li><code dir=\"ltr\">MyReturnType&lt;F&gt;</code>: the return type of a function type.</li>\n<li><code dir=\"ltr\">UnwrapPromise&lt;T&gt;</code>: the value inside a <code dir=\"ltr\">Promise</code>, or <code dir=\"ltr\">T</code> itself if it is not a promise.</li>\n<li><code dir=\"ltr\">OrderWithStatus&lt;S&gt;</code>: the <code dir=\"ltr\">Order</code> variant(s) whose <code dir=\"ltr\">status</code> is <code dir=\"ltr\">S</code>.</li>\n<li><code dir=\"ltr\">KeysOfType&lt;T, V&gt;</code>: the keys of <code dir=\"ltr\">T</code> whose values are assignable to <code dir=\"ltr\">V</code>.</li>\n</ul>\n","hints":["<code dir=\"ltr\">MyExclude</code> and <code dir=\"ltr\">MyExtract</code> are one-line distributive conditionals: one returns <code dir=\"ltr\">never</code> in the true branch, the other in the false branch.","Use <code dir=\"ltr\">infer</code> to capture a part of a type: <code dir=\"ltr\">F extends (...args: any[]) =&gt; infer R ? R : never</code>.","<code dir=\"ltr\">OrderWithStatus</code> is <code dir=\"ltr\">MyExtract&lt;Order, { status: S }&gt;</code>. For <code dir=\"ltr\">KeysOfType</code>, map each key to itself or <code dir=\"ltr\">never</code>, then index the result with <code dir=\"ltr\">[keyof T]</code>."],"explanation":"<p><code dir=\"ltr\">MyExclude</code> and <code dir=\"ltr\">MyExtract</code> rely on distribution: <code dir=\"ltr\">T</code> is a naked type parameter, so each union member is tested separately and the <code dir=\"ltr\">never</code> results vanish. The same distribution makes <code dir=\"ltr\">OrderWithStatus&lt;'delivered' | 'returned'&gt;</code> give both variants, which is why <code dir=\"ltr\">['deliveredAt']</code> works on it. <code dir=\"ltr\">infer</code> captures the return type and the promise’s value. <code dir=\"ltr\">KeysOfType</code> builds an object whose values are key names or <code dir=\"ltr\">never</code>, then reads all of its values at once with <code dir=\"ltr\">[keyof T]</code>.</p>\n","checkLabels":{"types":"The file type-checks with zero errors: all ten type tests pass"}},"words":777,"readMinutes":4}