{"id":"l-at-1-4","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Replace bags of optional fields with discriminated unions, so each order state carries exactly the data it has and impossible states stop compiling.","html":"<p>Here is how Cartwheel’s <code dir=\"ltr\">Order</code> type looked before anyone thought about 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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><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>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'returned'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  trackingNumber</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:#6E7781;--shiki-dark:#8B949E\">// set once shipped</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  deliveredAt</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:#6E7781;--shiki-dark:#8B949E\">// set once delivered</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  cancelReason</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// set if cancelled</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  refundCents</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\">// set if returned</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">};</span></span></code></pre></div>\n<p>It looks reasonable, and it is a trap. Four optional fields can each be present or absent, so this type allows 5 × 2⁴ = 80 shapes. Cartwheel’s business has five. A shipped order with no tracking number compiles. A cancelled order with a delivery date compiles. And every time you read <code dir=\"ltr\">order.trackingNumber</code>, you get <code dir=\"ltr\">string | undefined</code> and reach for <code dir=\"ltr\">!</code>, which is you telling the compiler “trust me” eighty times a week.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">One type per state</h2>\n<p>A <strong>discriminated union</strong> gives each state its own object type and joins them with <code dir=\"ltr\">|</code>. Each member has a shared property with a different literal value, the <strong>discriminant</strong>:</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\"> ProcessingOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</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\"> ShippedOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">trackingNumber</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeliveredOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">trackingNumber</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\">deliveredAt</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CancelledOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cancelReason</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ReturnedOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#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>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'returned'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  trackingNumber</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:#953800;--shiki-dark:#FFA657\">  deliveredAt</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:#953800;--shiki-dark:#FFA657\">  refundCents</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:#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\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ProcessingOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ShippedOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeliveredOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CancelledOrder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ReturnedOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n<p>Now the type allows exactly five shapes. Notice that nothing is optional any more: a shipped order <em>has</em> a tracking number. The impossible states are not checked at runtime; they are simply not expressible:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { id: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Type '{ id: number; status: \"shipped\"; }' is not assignable to type 'Order'.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">//   Property 'trackingNumber' is missing in type '{ id: number; status: \"shipped\"; }'</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">//   but required in type 'ShippedOrder'.</span></span></code></pre></div>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-at-1-4-t1\"><defs><marker id=\"arrow-l-at-1-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-at-1-4-t1\">Order state machine. Processing goes to shipped via ship, shipped goes to delivered via deliver, delivered goes to returned via refund. Processing can also go to cancelled via cancel. Each state box lists the fields it adds.</title>\n  <rect class=\"d-box-primary\" x=\"10\" y=\"40\" width=\"150\" height=\"64\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"85\" y=\"68\" text-anchor=\"middle\">processing</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"90\" text-anchor=\"middle\">id</text>\n  <rect class=\"d-box-accent\" x=\"190\" y=\"40\" width=\"150\" height=\"64\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"265\" y=\"68\" text-anchor=\"middle\">shipped</text>\n  <text class=\"d-label-muted\" x=\"265\" y=\"90\" text-anchor=\"middle\">+ trackingNumber</text>\n  <rect class=\"d-box-success\" x=\"370\" y=\"40\" width=\"150\" height=\"64\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"445\" y=\"68\" text-anchor=\"middle\">delivered</text>\n  <text class=\"d-label-muted\" x=\"445\" y=\"90\" text-anchor=\"middle\">+ deliveredAt</text>\n  <rect class=\"d-box\" x=\"550\" y=\"40\" width=\"140\" height=\"64\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"620\" y=\"68\" text-anchor=\"middle\">returned</text>\n  <text class=\"d-label-muted\" x=\"620\" y=\"90\" text-anchor=\"middle\">+ refundCents</text>\n  <rect class=\"d-box-warn\" x=\"10\" y=\"150\" width=\"150\" height=\"64\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"85\" y=\"178\" text-anchor=\"middle\">cancelled</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"200\" text-anchor=\"middle\">+ cancelReason</text>\n  <path class=\"d-arrow\" d=\"M160 72 L186 72\" marker-end=\"url(#arrow-l-at-1-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M340 72 L366 72\" marker-end=\"url(#arrow-l-at-1-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M520 72 L546 72\" marker-end=\"url(#arrow-l-at-1-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M85 104 L85 146\" marker-end=\"url(#arrow-l-at-1-4-1)\"/>\n  <text class=\"d-code\" x=\"173\" y=\"30\" text-anchor=\"middle\">ship</text>\n  <text class=\"d-code\" x=\"353\" y=\"30\" text-anchor=\"middle\">deliver</text>\n  <text class=\"d-code\" x=\"533\" y=\"30\" text-anchor=\"middle\">refund</text>\n  <text class=\"d-code\" x=\"100\" y=\"130\">cancel</text>\n</svg><figcaption>Each state is its own type; transitions are functions between them</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">Narrowing on the discriminant</h2>\n<p>Checking the discriminant narrows the whole object. Inside each <code dir=\"ltr\">case</code>, TypeScript knows exactly which member you have:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> describe</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\"> 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\"> (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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `#${</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\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} is being packed`</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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `#${</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\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} is on its way (${</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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `#${</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\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} arrived on ${</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\">deliveredAt</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\"> 'cancelled'</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:#0A3069;--shiki-dark:#A5D6FF\"> `#${</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\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} was 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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `#${</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\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} was refunded $${</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</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\">refundCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toFixed</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>No <code dir=\"ltr\">!</code>, no <code dir=\"ltr\">?.</code>, no <code dir=\"ltr\">?? ''</code>. And notice the explicit <code dir=\"ltr\">: string</code> return type. If someone adds an <code dir=\"ltr\">'on-hold'</code> status to the union next month, the end of this function becomes reachable, and TypeScript reports <em>Function lacks ending return statement and return type does not include ‘undefined’</em>. The compiler finds every place that needs updating. You will make this check explicit and louder with <code dir=\"ltr\">never</code> in <a href=\"/lesson/advanced-typescript/l-at-4-1\">Branded Types and Exhaustive Checks</a>.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Transitions as functions between states</h2>\n<p>The union pays off most in functions that change state. Type their inputs and outputs as specific variants:</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\"> ship</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\"> ProcessingOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">trackingNumber</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ShippedOrder</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\"> { id: order.id, status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, trackingNumber };</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\"> cancel</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\"> ProcessingOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">reason</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CancelledOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { id: order.id, status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'cancelled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, cancelReason: reason };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Now “you cannot ship an order twice” and “you cannot cancel a shipped order” are compile-time facts. A caller with an <code dir=\"ltr\">Order</code> in hand must narrow first:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> shipIfReady</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:#953800;--shiki-dark:#FFA657\">tracking</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (order.status </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</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:#1F2328;--shiki-dark:#E6EDF3\"> order;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> ship</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(order, tracking); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// order is ProcessingOrder here</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The return types matter as much as the parameters. Because <code dir=\"ltr\">ship</code> returns a <code dir=\"ltr\">ShippedOrder</code> rather than an <code dir=\"ltr\">Order</code>, the caller can hand the result straight to a function that needs a tracking number, such as <code dir=\"ltr\">printLabel(shipped)</code>, without checking <code dir=\"ltr\">status</code> again. Each transition carries the knowledge forward. Return the wide <code dir=\"ltr\">Order</code> and you throw that knowledge away, forcing every caller to narrow again.</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>A discriminant that is not a literal</span></p>\n<p>The pattern only works when each member’s discriminant is a literal type. If <code dir=\"ltr\">status</code> comes in typed as <code dir=\"ltr\">string</code> (say, from an untyped API response), checking it narrows nothing, and you are back to <code dir=\"ltr\">!</code> everywhere. Parse the response into the union at the edge of your app, which is the subject of <a href=\"/lesson/advanced-typescript/l-at-4-3\">Parsing Untrusted Data at the Boundary</a>, and keep <code dir=\"ltr\">string</code> out of the core.</p>\n</aside>\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>Name the variants</span></p>\n<p>You could write the union inline, but naming each member (<code dir=\"ltr\">ShippedOrder</code>, <code dir=\"ltr\">CancelledOrder</code>) gives you the vocabulary for transition functions and makes error messages readable. A message about <code dir=\"ltr\">ShippedOrder</code> is much easier to act on than one about a 200-character anonymous object type.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Sharing common fields</h2>\n<p>Real orders have more than an <code dir=\"ltr\">id</code>: a customer, a placed-at timestamp, line items. Repeating those in five variants invites drift, so pull them into a base type and intersect it with each state’s own fields:</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\"> OrderBase</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">customerId</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\">placedAt</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>\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\"> OrderBase</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;</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:#953800;--shiki-dark:#FFA657\">trackingNumber</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Cancelled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderBase</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cancelReason</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span></code></pre></div>\n<p>Each variant is still a separate member with its own literal <code dir=\"ltr\">status</code>, so narrowing works exactly as before. The base type also gives you a natural parameter type for functions that do not care about the state, such as <code dir=\"ltr\">function customerLink(order: OrderBase)</code>, which accepts every variant.</p>\n<p>You can also destructure the discriminant. Since TypeScript 4.4, <code dir=\"ltr\">const { status } = order; if (status === 'shipped') { … }</code> narrows <code dir=\"ltr\">order</code> too, as long as <code dir=\"ltr\">order</code> is a <code dir=\"ltr\">const</code> or an unassigned parameter. That keeps long <code dir=\"ltr\">switch</code> statements readable without giving up the narrowing.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">When not to use one</h2>\n<p>A discriminated union is right when states carry <em>different data</em>. When the variants differ only in a label (say, a <code dir=\"ltr\">channel</code> of <code dir=\"ltr\">'web' | 'mobile_app' | 'marketplace'</code> with the same fields), a plain literal union on one property is enough. Reach for the full pattern when you catch yourself writing optional fields with comments like “only set when…”.</p>\n<p>In the exercise, you will turn Cartwheel’s bag-of-optionals <code dir=\"ltr\">Order</code> into a union and make <code dir=\"ltr\">ship</code> accept only orders that can be shipped. Next lesson: literal types, <code dir=\"ltr\">as const</code> and <code dir=\"ltr\">satisfies</code>, the tools that keep those literals from widening back into <code dir=\"ltr\">string</code>.</p>\n","toc":[{"id":"sec-1","text":"One type per state","level":2},{"id":"sec-2","text":"Narrowing on the discriminant","level":2},{"id":"sec-3","text":"Transitions as functions between states","level":2},{"id":"sec-4","text":"Sharing common fields","level":2},{"id":"sec-5","text":"When not to use one","level":2}],"takeaways":["A type made of optional fields allows every combination of them, most of which are states your business can never be in.","A discriminated union gives each state its own object type, tied together by a shared literal property such as <code dir=\"ltr\">status</code>.","Checking the discriminant with <code dir=\"ltr\">===</code> or <code dir=\"ltr\">switch</code> narrows the whole object, so state-specific fields become available without <code dir=\"ltr\">!</code>.","Functions that move between states should accept and return specific variants, like <code dir=\"ltr\">ship(order: ProcessingOrder): ShippedOrder</code>.","An explicit return type on a <code dir=\"ltr\">switch</code> over the discriminant turns a forgotten case into a compile error."],"further":[{"title":"Discriminated unions (Narrowing)","url":"https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions"},{"title":"Unions and intersection types (Everyday Types)","url":"https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types"}],"quiz":[{"q":"<p>An <code dir=\"ltr\">Order</code> type has a <code dir=\"ltr\">status</code> with 5 values and 4 optional fields. Roughly how many shapes does the type allow, and how many of them are valid states if each status uses a fixed set of fields?</p>","options":[{"text":"5 allowed, 5 valid, because <code dir=\"ltr\">status</code> controls which fields exist.","why":"Nothing in an optional-field type ties fields to <code dir=\"ltr\">status</code>. The compiler sees them as independent."},{"text":"80 allowed (5 × 2⁴), 5 valid.","why":"Correct. Each optional field can be present or absent independently, so most combinations are impossible states that still compile."},{"text":"20 allowed (5 × 4), 5 valid.","why":"Optional fields multiply, they do not add. Four independent yes/no choices give 16 combinations per status."}],"answer":1},{"q":"<p>Given the union below, what is the type of <code dir=\"ltr\">order</code> inside the <code dir=\"ltr\">case 'returned':</code> branch?</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\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</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\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\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\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">deliveredAt</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\">  |</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\"> 'returned'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">deliveredAt</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\">refundCents</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></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">Order</code>, because <code dir=\"ltr\">switch</code> does not narrow objects","why":"Switching on a literal discriminant narrows the whole object, which is the point of the pattern."},{"text":"<code dir=\"ltr\">{ status: 'delivered' … } | { status: 'returned' … }</code>, because both have <code dir=\"ltr\">deliveredAt</code>","why":"Narrowing follows the discriminant value, not the shared fields. Only one member has <code dir=\"ltr\">status: 'returned'</code>."},{"text":"<code dir=\"ltr\">{ status: 'returned'; id: number; deliveredAt: string; refundCents: number }</code>","why":"Correct. Only that member’s <code dir=\"ltr\">status</code> can equal <code dir=\"ltr\">'returned'</code>, so <code dir=\"ltr\">refundCents</code> is available as a plain <code dir=\"ltr\">number</code>."}],"answer":2},{"q":"<p>Which signature best uses the union to prevent shipping an order twice?</p>","options":[{"text":"<code dir=\"ltr\">ship(order: Order, tracking: string): Order</code>","why":"This accepts a delivered or cancelled order too, and the caller loses the knowledge that the result is shipped."},{"text":"<code dir=\"ltr\">ship(order: Order, tracking: string): ShippedOrder</code>","why":"The return type is better, but any order, including one already shipped, can still be passed in."},{"text":"<code dir=\"ltr\">ship(order: ShippedOrder, tracking: string): ShippedOrder</code>","why":"This only accepts orders that are already shipped, which is the opposite of what the function should allow."},{"text":"<code dir=\"ltr\">ship(order: ProcessingOrder, tracking: string): ShippedOrder</code>","why":"Correct. Only a processing order can go in, and the caller gets back a value the compiler knows is shipped."}],"answer":3},{"q":"<p>You add an <code dir=\"ltr\">'on-hold'</code> status to the <code dir=\"ltr\">Order</code> union. Which function is guaranteed to get a compile error until you handle it?</p>","options":[{"text":"A function with return type <code dir=\"ltr\">string</code> whose <code dir=\"ltr\">switch</code> returns in every case and has no <code dir=\"ltr\">default</code>.","why":"Correct. With the new member unhandled, the end of the function is reachable, so TypeScript reports that it lacks an ending return statement."},{"text":"A function that uses <code dir=\"ltr\">if (order.status === 'shipped') … else …</code>.","why":"The <code dir=\"ltr\">else</code> branch silently absorbs the new status, which is exactly the bug you want to avoid."},{"text":"A function with a <code dir=\"ltr\">default:</code> branch that returns <code dir=\"ltr\">'Unknown'</code>.","why":"The <code dir=\"ltr\">default</code> handles the new status at runtime, so the compiler has no reason to complain."}],"answer":0}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"// Every field that only some statuses have is optional,\n// so impossible states (shipped with no tracking number…) compile.\ntype Order = {\n  id: number;\n  status: 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\n  trackingNumber?: string; // shipped, delivered, returned\n  deliveredAt?: string;    // delivered, returned\n  cancelReason?: string;   // cancelled\n  refundCents?: number;    // returned\n};\n\nfunction ship(order: Order, trackingNumber: string): Order {\n  return { id: order.id, status: 'shipped', trackingNumber };\n}\n\nfunction describe(order: Order): string {\n  switch (order.status) {\n    case 'processing': return `#${order.id} is being packed`;\n    case 'shipped': return `#${order.id} is on its way (${order.trackingNumber})`;\n    case 'delivered': return `#${order.id} arrived on ${order.deliveredAt}`;\n    case 'cancelled': return `#${order.id} was cancelled: ${order.cancelReason}`;\n    case 'returned': return `#${order.id} was refunded $${(order.refundCents! / 100).toFixed(2)}`;\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<Order['status'], 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned'>>;\n\n// @ts-expect-error: a shipped order must have a tracking number\nconst noTracking: Order = { id: 1, status: 'shipped' };\n\n// @ts-expect-error: a cancelled order cannot have a delivery date\nconst cancelledButDelivered: Order = { id: 2, status: 'cancelled', cancelReason: 'Out of stock', deliveredAt: '2025-05-02' };\n\n// @ts-expect-error: only processing orders can be shipped\nship({ id: 3, status: 'delivered', trackingNumber: '1Z-77', deliveredAt: '2025-05-02' }, '1Z-88');\n\nconsole.log(describe(ship({ id: 1042, status: 'processing' }, '1Z-999')));\n"},"solution":{"ts":"type ProcessingOrder = { id: number; status: 'processing' };\ntype ShippedOrder = { id: number; status: 'shipped'; trackingNumber: string };\ntype DeliveredOrder = { id: number; status: 'delivered'; trackingNumber: string; deliveredAt: string };\ntype CancelledOrder = { id: number; status: 'cancelled'; cancelReason: string };\ntype ReturnedOrder = {\n  id: number;\n  status: 'returned';\n  trackingNumber: string;\n  deliveredAt: string;\n  refundCents: number;\n};\n\ntype Order = ProcessingOrder | ShippedOrder | DeliveredOrder | CancelledOrder | ReturnedOrder;\n\nfunction ship(order: ProcessingOrder, trackingNumber: string): ShippedOrder {\n  return { id: order.id, status: 'shipped', trackingNumber };\n}\n\nfunction describe(order: Order): string {\n  switch (order.status) {\n    case 'processing': return `#${order.id} is being packed`;\n    case 'shipped': return `#${order.id} is on its way (${order.trackingNumber})`;\n    case 'delivered': return `#${order.id} arrived on ${order.deliveredAt}`;\n    case 'cancelled': return `#${order.id} was cancelled: ${order.cancelReason}`;\n    case 'returned': return `#${order.id} was refunded $${(order.refundCents / 100).toFixed(2)}`;\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<Order['status'], 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned'>>;\n\n// @ts-expect-error: a shipped order must have a tracking number\nconst noTracking: Order = { id: 1, status: 'shipped' };\n\n// @ts-expect-error: a cancelled order cannot have a delivery date\nconst cancelledButDelivered: Order = { id: 2, status: 'cancelled', cancelReason: 'Out of stock', deliveredAt: '2025-05-02' };\n\n// @ts-expect-error: only processing orders can be shipped\nship({ id: 3, status: 'delivered', trackingNumber: '1Z-77', deliveredAt: '2025-05-02' }, '1Z-88');\n\nconsole.log(describe(ship({ id: 1042, status: 'processing' }, '1Z-999')));\n"},"checks":[{"id":"types","typecheck":true},{"id":"ships","test":"const s = ship({ id: 7, status: 'processing' }, 'TRK-1'); return s.status === 'shipped' && s.trackingNumber === 'TRK-1' && s.id === 7;"},{"id":"describes","test":"return describe({ id: 9, status: 'returned', trackingNumber: 'T', deliveredAt: '2025-05-02', refundCents: 1250 }).includes('12.50') && describe({ id: 9, status: 'cancelled', cancelReason: 'Out of stock' }).includes('Out of stock');"}],"title":"Turn the order into a discriminated union","prompt":"<p>Replace the bag-of-optionals <code dir=\"ltr\">Order</code> with a discriminated union of five variants (one per <code dir=\"ltr\">status</code>), where every field a state has is required and fields it does not have are absent:</p>\n<ul>\n<li><code dir=\"ltr\">processing</code>: <code dir=\"ltr\">id</code></li>\n<li><code dir=\"ltr\">shipped</code>: <code dir=\"ltr\">id</code>, <code dir=\"ltr\">trackingNumber</code></li>\n<li><code dir=\"ltr\">delivered</code>: <code dir=\"ltr\">id</code>, <code dir=\"ltr\">trackingNumber</code>, <code dir=\"ltr\">deliveredAt</code></li>\n<li><code dir=\"ltr\">cancelled</code>: <code dir=\"ltr\">id</code>, <code dir=\"ltr\">cancelReason</code></li>\n<li><code dir=\"ltr\">returned</code>: <code dir=\"ltr\">id</code>, <code dir=\"ltr\">trackingNumber</code>, <code dir=\"ltr\">deliveredAt</code>, <code dir=\"ltr\">refundCents</code></li>\n</ul>\n<p>Then change <code dir=\"ltr\">ship</code> so it only accepts a processing order and returns a shipped one. The three <code dir=\"ltr\">@ts-expect-error</code> lines must become real errors, and <code dir=\"ltr\">describe</code> should no longer need <code dir=\"ltr\">!</code>.</p>\n","hints":["Write one object type per state, each with <code dir=\"ltr\">status</code> set to a single string literal, and join them with <code dir=\"ltr\">|</code>.","Naming the variants (<code dir=\"ltr\">ProcessingOrder</code>, <code dir=\"ltr\">ShippedOrder</code>…) lets you use them in <code dir=\"ltr\">ship</code>’s signature.","<code dir=\"ltr\">function ship(order: ProcessingOrder, trackingNumber: string): ShippedOrder</code>"],"explanation":"<p>Each variant now carries exactly the data its state has, so an object literal without <code dir=\"ltr\">trackingNumber</code> cannot be a shipped order, and an excess <code dir=\"ltr\">deliveredAt</code> on a cancelled order is flagged. Inside each <code dir=\"ltr\">case</code>, the <code dir=\"ltr\">status</code> check narrows <code dir=\"ltr\">order</code> to one variant, so <code dir=\"ltr\">refundCents</code> is a plain <code dir=\"ltr\">number</code> and the <code dir=\"ltr\">!</code> is unnecessary. Typing <code dir=\"ltr\">ship</code> with <code dir=\"ltr\">ProcessingOrder</code> in and <code dir=\"ltr\">ShippedOrder</code> out moves the rule “only processing orders ship” from a code review comment into the compiler.</p>\n","checkLabels":{"types":"The file type-checks with zero errors: all three impossible states are rejected","ships":"<code dir=\"ltr\">ship</code> returns a shipped order with the tracking number and the same id","describes":"<code dir=\"ltr\">describe</code> still describes returned and cancelled orders"}},"words":719,"readMinutes":3}