{"id":"l-at-4-1","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Give same-shaped values distinct types with brands and smart constructors, stop mixing currencies at compile time, and make every switch over a union provably exhaustive with never.","html":"<p>The refund bug from the first lesson came down to two numbers that looked the same: dollars and cents. Cartwheel has the same problem everywhere. An order id and a customer id are both <code dir=\"ltr\">number</code>. An amount in cents and a quantity are both <code dir=\"ltr\">number</code>. Structural typing, which you met in <a href=\"/lesson/advanced-typescript/l-at-1-2\">Structural Typing and Assignability</a>, considers same-shaped things identical, so it happily lets you write <code dir=\"ltr\">cancelOrder(customer.id)</code>.</p>\n<p>The fix is to make the shapes differ, without changing anything at runtime.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Brands</h2>\n<p>A <strong>branded type</strong> intersects a primitive with a property that only exists in the type system:</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\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> brand</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> unique</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> symbol</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\"> Brand</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\">B</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 style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">readonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [brand]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> B</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\"> OrderId</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'OrderId'</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\"> CustomerId</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'CustomerId'</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\"> Cents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Cents'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span></code></pre></div>\n<p><code dir=\"ltr\">declare const brand: unique symbol</code> declares a symbol that never exists at runtime; using it as the property key means no real object can accidentally have it, and nothing outside this file can forge it by name. An <code dir=\"ltr\">OrderId</code> is still a number for every purpose (you can compare it, print it, send it as JSON), but TypeScript no longer accepts a plain number or a <code dir=\"ltr\">CustomerId</code> in its place:</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> cancelOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderId</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\">cancelOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(customer.id);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type 'CustomerId' is not assignable to parameter of type 'OrderId'.</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">cancelOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type 'number' is not assignable to parameter of type 'OrderId'.</span></span></code></pre></div>\n<h2 id=\"sec-2\" tabindex=\"-1\">Smart constructors</h2>\n<p>If a plain number is not an <code dir=\"ltr\">OrderId</code>, where do <code dir=\"ltr\">OrderId</code>s come from? From exactly one function per brand, a <strong>smart constructor</strong> that validates and then casts:</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> toOrderId</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</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\"> OrderId</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isInteger</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(n) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Invalid order id: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">n</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderId</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> toCents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</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\"> Cents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isSafeInteger</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(n)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Cents must be a whole number, got ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">n</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Cents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>This is the one legitimate home for <code dir=\"ltr\">as</code> in branded code. Because the cast only happens after validation, every <code dir=\"ltr\">OrderId</code> anywhere in the system is known to be a valid id. A brand is a receipt: it proves the value went through the check.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 200\" role=\"img\" aria-labelledby=\"l-at-4-1-t1\"><defs><marker id=\"arrow-l-at-4-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-at-4-1-t1\">A plain number from a URL or JSON passes through the toOrderId smart constructor, which validates it and either throws or returns an OrderId. Functions such as cancelOrder accept only OrderId, so the plain number cannot reach them directly; that path is blocked.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"70\" width=\"150\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"85\" y=\"96\" text-anchor=\"middle\">number</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"116\" text-anchor=\"middle\">from URL, JSON</text>\n  <path class=\"d-arrow\" d=\"M160 98 L248 98\" marker-end=\"url(#arrow-l-at-4-1-1)\"/>\n  <rect class=\"d-box-warn\" x=\"252\" y=\"62\" width=\"180\" height=\"72\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"342\" y=\"92\" text-anchor=\"middle\">toOrderId(n)</text>\n  <text class=\"d-label-muted\" x=\"342\" y=\"116\" text-anchor=\"middle\">validate, then cast</text>\n  <path class=\"d-arrow\" d=\"M432 98 L510 98\" marker-end=\"url(#arrow-l-at-4-1-1)\"/>\n  <rect class=\"d-box-success\" x=\"514\" y=\"70\" width=\"176\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"602\" y=\"96\" text-anchor=\"middle\">cancelOrder(id)</text>\n  <text class=\"d-label-muted\" x=\"602\" y=\"116\" text-anchor=\"middle\">takes OrderId</text>\n  <path class=\"d-line d-dashed\" d=\"M85 126 L85 170 L602 170 L602 126\"/>\n  <text class=\"d-label-muted\" x=\"342\" y=\"162\" text-anchor=\"middle\">direct path: compile error</text>\n</svg><figcaption>Smart constructors are the only gate into a branded type</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">Money that refuses to mix</h2>\n<p>Branding cents is half the job. The other half is currency. Make the currency a type parameter, so <code dir=\"ltr\">Money&lt;'USD'&gt;</code> and <code dir=\"ltr\">Money&lt;'EUR'&gt;</code> are different types:</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\"> Currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'EGP'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'AED'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'SAR'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'JOD'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'GBP'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'EUR'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'USD'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'CAD'</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\"> Money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Currency</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\">cents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Cents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">currency</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> C</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Currency</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</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\"> Money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>, </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\"> Money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">NoInfer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>>)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">C</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\"> { cents: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toCents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(a.cents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> b.cents), currency: a.currency };</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\">add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(usdPrice, usdShipping); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Money&#x3C;'USD'></span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(usdPrice, eurShipping);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type 'Money&#x3C;\"EUR\">' is not assignable to parameter of type 'Money&#x3C;\"USD\">'.</span></span></code></pre></div>\n<p>Two details matter. Without <code dir=\"ltr\">NoInfer</code>, both arguments would be inference sites and <code dir=\"ltr\">C</code> would quietly become <code dir=\"ltr\">'USD' | 'EUR'</code>, the exact trap from <a href=\"/lesson/advanced-typescript/l-at-2-3\">Controlling Inference</a>. And <code dir=\"ltr\">a.cents + b.cents</code> is a plain <code dir=\"ltr\">number</code>: arithmetic strips brands, because TypeScript has no idea whether the result still means “cents”. Re-brand deliberately, through the constructor, so the rule “cents are whole numbers” is checked again.</p>\n<h3 id=\"sec-4\" tabindex=\"-1\">Where brands pay off</h3>\n<p>Do not brand everything. Each brand adds a constructor call wherever values enter the system, and a test fixture has to go through it too. Brand where two values share a primitive type, are easy to confuse, and are expensive to confuse: ids of different entities, amounts in different units, and strings that must pass validation before use, such as an <code dir=\"ltr\">Email</code> or a <code dir=\"ltr\">Sku</code>. A <code dir=\"ltr\">quantity</code> that is only ever multiplied by a price does not need one. On a large codebase, a dozen well-chosen brands catch most of the “right type, wrong meaning” bugs; a hundred of them mostly generate casts.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Expecting brands to survive the network</span></p>\n<p>Brands exist only at compile time. A value from <code dir=\"ltr\">JSON.parse</code>, a URL parameter or <code dir=\"ltr\">localStorage</code> is a plain number, whatever your types say about the endpoint. If you type an API response as <code dir=\"ltr\">{ id: OrderId }</code> and skip parsing, you have cast unvalidated data into a brand, which is worse than no brand at all. Brand values while parsing them at the boundary, which is the subject of <a href=\"/lesson/advanced-typescript/l-at-4-3\">Parsing Untrusted Data at the Boundary</a>.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Exhaustiveness with never</h2>\n<p>Brands make values distinct. The other everyday pattern makes <em>handling</em> complete. You saw in <a href=\"/lesson/advanced-typescript/l-at-1-4\">Discriminated Unions for Domain States</a> that a missing return can reveal a missed case. A <code dir=\"ltr\">never</code> check makes that explicit and works even when the function returns nothing:</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> assertNever</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</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\">  throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Unhandled value: ${</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">value</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> notifyCustomer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span 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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">  switch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (order.status) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'We are packing your order'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Track it: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">order</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">trackingNumber</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Enjoy!'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Cancelled: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">order</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cancelReason</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'returned'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Refund on its way'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    default</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> assertNever</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(order);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>In the <code dir=\"ltr\">default</code> branch, every handled member has been narrowed away, so <code dir=\"ltr\">order</code> is <code dir=\"ltr\">never</code>, and passing it to a <code dir=\"ltr\">never</code> parameter compiles. Add an <code dir=\"ltr\">'on-hold'</code> status and <code dir=\"ltr\">order</code> becomes the on-hold variant in that branch, which is not assignable to <code dir=\"ltr\">never</code>: compile error, pointing at the exact switch to update. At runtime, if bad data ever sneaks past the types, the function throws with a clear message instead of doing nothing.</p>\n<p>If you prefer not to define a helper, <code dir=\"ltr\">default: throw new Error(`Unhandled: ${order satisfies never}`)</code> does the same check inline. And for lookups, <code dir=\"ltr\">Record&lt;OrderStatus, string&gt;</code> with <code dir=\"ltr\">satisfies</code> gives you exhaustiveness for free: a missing key is a compile error.</p>\n<p>In the exercise you will brand Cartwheel’s ids and cents, make <code dir=\"ltr\">add</code> reject mixed currencies, and add an exhaustive check. Next lesson tackles the other thing every checkout must handle: failures you expect.</p>\n","toc":[{"id":"sec-1","text":"Brands","level":2},{"id":"sec-2","text":"Smart constructors","level":2},{"id":"sec-3","text":"Money that refuses to mix","level":2},{"id":"sec-4","text":"Where brands pay off","level":3},{"id":"sec-5","text":"Exhaustiveness with never","level":2}],"takeaways":["A brand intersects a primitive with a unique, type-only property, so an <code dir=\"ltr\">OrderId</code> and a <code dir=\"ltr\">CustomerId</code> stop being interchangeable even though both are numbers.","Create branded values only in smart constructors that validate first; that is the one place an <code dir=\"ltr\">as</code> cast belongs.","Arithmetic on branded numbers returns plain <code dir=\"ltr\">number</code>, so money helpers must re-brand their results deliberately.","<code dir=\"ltr\">Money&lt;NoInfer&lt;C&gt;&gt;</code> on the second argument stops <code dir=\"ltr\">add(usd, eur)</code> from inferring a mixed-currency union.","A <code dir=\"ltr\">default</code> branch that passes the value to a <code dir=\"ltr\">never</code> parameter, or checks it with <code dir=\"ltr\">satisfies never</code>, turns a missed union member into a compile error."],"further":[{"title":"Unique symbol types","url":"https://www.typescriptlang.org/docs/handbook/symbols.html#unique-symbol"},{"title":"Exhaustiveness checking (Narrowing)","url":"https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking"}],"quiz":[{"q":"<p><code dir=\"ltr\">type OrderId = number &amp; { readonly [brand]: 'OrderId' }</code>. What does the brand property look like at runtime?</p>","options":[{"text":"Every <code dir=\"ltr\">OrderId</code> value carries a hidden symbol property.","why":"Primitives cannot carry properties, and the brand is only declared, never assigned. It exists in the type system alone."},{"text":"The value is boxed into a <code dir=\"ltr\">Number</code> object with the symbol attached.","why":"Nothing boxes the value. A cast like <code dir=\"ltr\">n as OrderId</code> emits no code at all."},{"text":"It does not exist; the value is a plain number, and the brand lives only in the type.","why":"Correct. That is why brands are free at runtime, and also why data from <code dir=\"ltr\">JSON.parse</code> is never branded until you construct it."}],"answer":2},{"q":"<p>Where should <code dir=\"ltr\">as OrderId</code> appear in a well-designed codebase?</p>","options":[{"text":"Wherever a number needs to be passed to a function that takes an <code dir=\"ltr\">OrderId</code>.","why":"That turns the brand into decoration; any number can be cast anywhere and the guarantee is gone."},{"text":"Only inside the smart constructor that validates the number first.","why":"Correct. One validated entry point means every <code dir=\"ltr\">OrderId</code> in the system has been checked."},{"text":"Nowhere; brands should be created with <code dir=\"ltr\">satisfies</code>.","why":"<code dir=\"ltr\">satisfies</code> checks against a type without changing it, so it cannot add a brand that the value does not have."}],"answer":1},{"q":"<p><code dir=\"ltr\">add&lt;C extends Currency&gt;(a: Money&lt;C&gt;, b: Money&lt;C&gt;)</code> is called as <code dir=\"ltr\">add(usd, eur)</code>. What happens?</p>","options":[{"text":"It is a compile error, because the currencies differ.","why":"That is what you want, but both arguments are inference sites, so <code dir=\"ltr\">C</code> becomes <code dir=\"ltr\">'USD' | 'EUR'</code> and the call compiles."},{"text":"It compiles, because <code dir=\"ltr\">C</code> is inferred as <code dir=\"ltr\">'USD' | 'EUR'</code>; mark the second parameter <code dir=\"ltr\">Money&lt;NoInfer&lt;C&gt;&gt;</code> to reject it.","why":"Correct. With <code dir=\"ltr\">NoInfer</code>, <code dir=\"ltr\">C</code> comes from the first argument only, and <code dir=\"ltr\">Money&lt;'EUR'&gt;</code> fails against <code dir=\"ltr\">Money&lt;'USD'&gt;</code>."},{"text":"It throws at runtime because the brand does not match.","why":"Brands do not exist at runtime, so nothing can throw because of one."}],"answer":1},{"q":"<p>A new <code dir=\"ltr\">'on-hold'</code> status is added. Which function fails to compile until it handles it?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">s</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  switch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (s) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Packing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">    // …one case for every other existing status…</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    default</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> assertNever</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(s);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"This one, because in the <code dir=\"ltr\">default</code> branch <code dir=\"ltr\">s</code> is <code dir=\"ltr\">'on-hold'</code>, which is not assignable to <code dir=\"ltr\">never</code>.","why":"Correct. The <code dir=\"ltr\">default</code> branch only sees the members no case handled, and <code dir=\"ltr\">assertNever</code> accepts none."},{"text":"None; <code dir=\"ltr\">default</code> handles every value, so the compiler is satisfied.","why":"A plain <code dir=\"ltr\">default</code> would hide the new status. Passing the value to a <code dir=\"ltr\">never</code> parameter is what makes it a check."},{"text":"Every function that uses <code dir=\"ltr\">OrderStatus</code>, including ones that only store it.","why":"Only code that claims exhaustiveness is affected. Functions that store or pass the status through keep compiling."},{"text":"Only functions that use <code dir=\"ltr\">if</code> statements instead of <code dir=\"ltr\">switch</code>","why":"The check is about the <code dir=\"ltr\">never</code> parameter, not the statement used; it works with <code dir=\"ltr\">if</code> chains too."}],"answer":0}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"// Brand these three so they stop being interchangeable numbers.\ntype OrderId = number;\ntype CustomerId = number;\ntype Cents = number;\n\nfunction toOrderId(n: number): OrderId {\n  if (!Number.isInteger(n) || n < 1000) throw new Error(`Invalid order id: ${n}`);\n  return n;\n}\n\nfunction toCustomerId(n: number): CustomerId {\n  if (!Number.isInteger(n) || n < 1) throw new Error(`Invalid customer id: ${n}`);\n  return n;\n}\n\nfunction toCents(n: number): Cents {\n  if (!Number.isSafeInteger(n)) throw new Error(`Cents must be a whole number, got ${n}`);\n  return n;\n}\n\ntype Currency = 'USD' | 'EUR' | 'EGP';\ntype Money<C extends Currency = Currency> = { cents: Cents; currency: C };\n\nfunction money<C extends Currency>(cents: number, currency: C): Money<C> {\n  return { cents: toCents(cents), currency };\n}\n\nfunction add<C extends Currency>(a: Money<C>, b: Money<C>): Money<C> {\n  return { cents: toCents(a.cents + b.cents), currency: a.currency };\n}\n\ntype PaymentMethod =\n  | { kind: 'card'; last4: string }\n  | { kind: 'wallet'; provider: 'apple' | 'google' }\n  | { kind: 'cash-on-delivery' };\n\nfunction describePayment(p: PaymentMethod): string {\n  switch (p.kind) {\n    case 'card': return `Card ending ${p.last4}`;\n    case 'wallet': return `${p.provider === 'apple' ? 'Apple' : 'Google'} wallet`;\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<NotEqual<OrderId, number>>;\ntype T2 = Expect<NotEqual<OrderId, CustomerId>>;\ntype T3 = Expect<Equal<OrderId extends number ? true : false, true>>;\n\nfunction typeTests(customer: CustomerId, usd: Money<'USD'>, eur: Money<'EUR'>) {\n  // @ts-expect-error: a customer id is not an order id\n  const fromCustomer: OrderId = customer;\n  // @ts-expect-error: plain numbers must go through toOrderId\n  const fromNumber: OrderId = 1042;\n  // @ts-expect-error: dollars and euros cannot be added\n  add(usd, eur);\n  const total = add(usd, usd);\n  type T4 = Expect<Equal<typeof total, Money<'USD'>>>;\n}\n\nconsole.log(add(money(18900, 'USD'), money(499, 'USD')), describePayment({ kind: 'cash-on-delivery' }));\n"},"solution":{"ts":"declare const brand: unique symbol;\ntype Brand<T, B extends string> = T & { readonly [brand]: B };\n\ntype OrderId = Brand<number, 'OrderId'>;\ntype CustomerId = Brand<number, 'CustomerId'>;\ntype Cents = Brand<number, 'Cents'>;\n\nfunction toOrderId(n: number): OrderId {\n  if (!Number.isInteger(n) || n < 1000) throw new Error(`Invalid order id: ${n}`);\n  return n as OrderId;\n}\n\nfunction toCustomerId(n: number): CustomerId {\n  if (!Number.isInteger(n) || n < 1) throw new Error(`Invalid customer id: ${n}`);\n  return n as CustomerId;\n}\n\nfunction toCents(n: number): Cents {\n  if (!Number.isSafeInteger(n)) throw new Error(`Cents must be a whole number, got ${n}`);\n  return n as Cents;\n}\n\ntype Currency = 'USD' | 'EUR' | 'EGP';\ntype Money<C extends Currency = Currency> = { cents: Cents; currency: C };\n\nfunction money<C extends Currency>(cents: number, currency: C): Money<C> {\n  return { cents: toCents(cents), currency };\n}\n\nfunction add<C extends Currency>(a: Money<C>, b: Money<NoInfer<C>>): Money<C> {\n  return { cents: toCents(a.cents + b.cents), currency: a.currency };\n}\n\ntype PaymentMethod =\n  | { kind: 'card'; last4: string }\n  | { kind: 'wallet'; provider: 'apple' | 'google' }\n  | { kind: 'cash-on-delivery' };\n\nfunction assertNever(value: never): never {\n  throw new Error(`Unhandled value: ${JSON.stringify(value)}`);\n}\n\nfunction describePayment(p: PaymentMethod): string {\n  switch (p.kind) {\n    case 'card': return `Card ending ${p.last4}`;\n    case 'wallet': return `${p.provider === 'apple' ? 'Apple' : 'Google'} wallet`;\n    case 'cash-on-delivery': return 'Cash on delivery';\n    default: return assertNever(p);\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<NotEqual<OrderId, number>>;\ntype T2 = Expect<NotEqual<OrderId, CustomerId>>;\ntype T3 = Expect<Equal<OrderId extends number ? true : false, true>>;\n\nfunction typeTests(customer: CustomerId, usd: Money<'USD'>, eur: Money<'EUR'>) {\n  // @ts-expect-error: a customer id is not an order id\n  const fromCustomer: OrderId = customer;\n  // @ts-expect-error: plain numbers must go through toOrderId\n  const fromNumber: OrderId = 1042;\n  // @ts-expect-error: dollars and euros cannot be added\n  add(usd, eur);\n  const total = add(usd, usd);\n  type T4 = Expect<Equal<typeof total, Money<'USD'>>>;\n}\n\nconsole.log(add(money(18900, 'USD'), money(499, 'USD')), describePayment({ kind: 'cash-on-delivery' }));\n"},"checks":[{"id":"types","typecheck":true},{"id":"validates","test":"let threw = false; try { toOrderId(999); } catch { threw = true; } return threw && toOrderId(1042) === 1042;"},{"id":"adds","test":"const t = add(money(18900, 'USD'), money(499, 'USD')); return t.cents === 19399 && t.currency === 'USD';"},{"id":"cash","test":"return describePayment({ kind: 'cash-on-delivery' }) === 'Cash on delivery';"},{"id":"throws-unknown","test":"try { describePayment({ kind: 'crypto' }); return false; } catch { return true; }"}],"title":"Brand the ids, lock the currency","prompt":"<p>Harden Cartwheel’s checkout types:</p>\n<ol>\n<li>Brand <code dir=\"ltr\">OrderId</code>, <code dir=\"ltr\">CustomerId</code> and <code dir=\"ltr\">Cents</code> so they are distinct from <code dir=\"ltr\">number</code> and from each other, and make the three smart constructors return branded values.</li>\n<li>Make <code dir=\"ltr\">add</code> reject <code dir=\"ltr\">Money</code> values in different currencies.</li>\n<li>Make <code dir=\"ltr\">describePayment</code> handle <code dir=\"ltr\">cash-on-delivery</code> (return <code dir=\"ltr\">'Cash on delivery'</code>) and add an exhaustiveness check that fails to compile if a payment kind is ever unhandled, and throws at runtime for an unknown kind.</li>\n</ol>\n","hints":["A brand is an intersection with a type-only property: <code dir=\"ltr\">declare const brand: unique symbol; type Brand&lt;T, B extends string&gt; = T &amp; { readonly [brand]: B };</code>.","The smart constructors are the one place to cast: <code dir=\"ltr\">return n as OrderId;</code>. For <code dir=\"ltr\">add</code>, remember why <code dir=\"ltr\">C</code> turns into a union: both arguments are inference sites.","Write <code dir=\"ltr\">function assertNever(value: never): never { throw … }</code> and call it in a <code dir=\"ltr\">default:</code> branch with the narrowed value."],"explanation":"<p>The intersection with a <code dir=\"ltr\">unique symbol</code> property makes each branded type a strict subset of <code dir=\"ltr\">number</code> that no plain number (or other brand) can satisfy, while costing nothing at runtime. Casting only inside validating constructors means every branded value has been checked. <code dir=\"ltr\">Money&lt;NoInfer&lt;C&gt;&gt;</code> makes the second argument check-only, so <code dir=\"ltr\">C</code> comes from the first argument and <code dir=\"ltr\">Money&lt;'EUR'&gt;</code> is rejected. In <code dir=\"ltr\">describePayment</code>, the <code dir=\"ltr\">default</code> branch receives whatever the cases did not handle; when that is nothing, it is <code dir=\"ltr\">never</code> and <code dir=\"ltr\">assertNever</code> compiles, and if a kind slips through at runtime it throws instead of returning <code dir=\"ltr\">undefined</code>.</p>\n","checkLabels":{"types":"The file type-checks with zero errors, including all type tests","validates":"<code dir=\"ltr\">toOrderId</code> rejects 999 and accepts 1042","adds":"<code dir=\"ltr\">add</code> sums two amounts in the same currency","cash":"<code dir=\"ltr\">describePayment</code> handles cash on delivery","throws-unknown":"<code dir=\"ltr\">describePayment</code> throws for an unknown payment kind"}},"words":729,"readMinutes":3}