{"id":"l-at-1-3","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Use typeof, equality, in, instanceof and early returns to narrow wide types, and recognise the places where TypeScript forgets a narrowing so you can keep it.","html":"<p>A Cartwheel order has <code dir=\"ltr\">shippingFeeCents: number | null</code>. <code dir=\"ltr\">null</code> means the fee is not known yet (the address is incomplete). <code dir=\"ltr\">0</code> means free shipping. Here is the label shown at checkout, as it was first written:</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\"> shippingLabel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">feeCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span 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\">  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\">feeCents) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Calculated at checkout'</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\"> (feeCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\"> 'Free'</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:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">feeCents</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></code></pre></div>\n<p>Every free-shipping customer saw “Calculated at checkout”. TypeScript did not complain, because after the first line <code dir=\"ltr\">feeCents</code> is <code dir=\"ltr\">number</code>, and comparing a number with <code dir=\"ltr\">0</code> is perfectly legal. The types were right; the narrowing was wrong. This lesson is about how TypeScript narrows, so you can make it work for you instead of trusting it blindly.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Control-flow analysis</h2>\n<p>TypeScript tracks the type of each variable through every branch and return. The same name can have a different type on each line:</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\"> shippingLabel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">feeCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // feeCents: number | null</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (feeCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Calculated at checkout'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // feeCents: number</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (feeCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\"> 'Free'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // feeCents: number</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:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">feeCents</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></code></pre></div>\n<p>This is <strong>control-flow analysis</strong>. Each check splits the flow into branches, and on each branch the variable’s type is the set of values that could still be there. When a branch returns, the code after it only sees what is left.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 300\" role=\"img\" aria-labelledby=\"l-at-1-3-t1\"><defs><marker id=\"arrow-l-at-1-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-at-1-3-t1\">Control flow of shippingLabel. The parameter starts as number or null. The check feeCents equals null branches to a return with type null. The remaining path has type number, then the check feeCents equals 0 branches to a return for free shipping, and the final path is a positive fee of type number.</title>\n  <rect class=\"d-box-accent\" x=\"230\" y=\"14\" width=\"220\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"42\" text-anchor=\"middle\">number | null</text>\n  <path class=\"d-arrow\" d=\"M340 58 L340 92\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <rect class=\"d-box-warn\" x=\"230\" y=\"96\" width=\"220\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"121\" text-anchor=\"middle\">feeCents === null ?</text>\n  <path class=\"d-arrow\" d=\"M230 116 L110 116 L110 150\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"160\" y=\"108\" text-anchor=\"middle\">yes</text>\n  <rect class=\"d-box\" x=\"20\" y=\"154\" width=\"180\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"110\" y=\"181\" text-anchor=\"middle\">null → \"Calculated…\"</text>\n  <path class=\"d-arrow\" d=\"M340 136 L340 170\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"356\" y=\"158\">no</text>\n  <rect class=\"d-box-warn\" x=\"230\" y=\"174\" width=\"220\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"199\" text-anchor=\"middle\">number: === 0 ?</text>\n  <path class=\"d-arrow\" d=\"M450 194 L570 194 L570 228\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"510\" y=\"186\" text-anchor=\"middle\">yes</text>\n  <rect class=\"d-box\" x=\"480\" y=\"232\" width=\"180\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"570\" y=\"259\" text-anchor=\"middle\">0 → \"Free\"</text>\n  <path class=\"d-arrow\" d=\"M340 214 L340 248\" marker-end=\"url(#arrow-l-at-1-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"230\" y=\"252\" width=\"220\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"277\" text-anchor=\"middle\">number → \"$4.99\"</text>\n</svg><figcaption>Each check splits the flow; the type after it is what can still be there</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">The narrowing toolbox</h2>\n<p>You narrow with ordinary JavaScript checks; TypeScript understands each one:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Check</th>\n<th>Narrows</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">typeof x === 'string'</code></td>\n<td>primitives</td>\n<td><code dir=\"ltr\">typeof amount === 'number'</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">x === null</code>, <code dir=\"ltr\">x !== undefined</code></td>\n<td>literal values</td>\n<td><code dir=\"ltr\">coupon !== null</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">'prop' in x</code></td>\n<td>object unions by property</td>\n<td><code dir=\"ltr\">'trackingNumber' in shipment</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">x instanceof C</code></td>\n<td>class instances</td>\n<td><code dir=\"ltr\">err instanceof TypeError</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">Array.isArray(x)</code></td>\n<td>arrays</td>\n<td><code dir=\"ltr\">Array.isArray(input)</code></td>\n</tr>\n</tbody>\n</table></div><p>Truthiness (<code dir=\"ltr\">if (x)</code>) also narrows, and it is the one to be careful with. It removes <code dir=\"ltr\">null</code> and <code dir=\"ltr\">undefined</code>, but also <code dir=\"ltr\">0</code>, <code dir=\"ltr\">''</code>, <code dir=\"ltr\">NaN</code> and <code dir=\"ltr\">false</code>. For an object or an array, a truthiness check is fine. For a number or a string where zero or empty means something, compare against <code dir=\"ltr\">null</code> explicitly.</p>\n<h3 id=\"sec-3\" tabindex=\"-1\"><code dir=\"ltr\">in</code> and <code dir=\"ltr\">instanceof</code> in practice</h3>\n<p>Two checks deserve a closer look because the checkout client uses them constantly. The payment provider answers with one of two shapes, and only one has a <code dir=\"ltr\">receiptId</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentReply</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">receiptId</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\">capturedCents</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:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">declineCode</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> summarize</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">reply</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentReply</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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'receiptId'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> reply) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `Paid, receipt ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">reply</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">receiptId</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:#0A3069;--shiki-dark:#A5D6FF\"> `Declined (${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">reply</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">declineCode</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></code></pre></div>\n<p><code dir=\"ltr\">'receiptId' in reply</code> keeps only the union members that declare <code dir=\"ltr\">receiptId</code>. It works, but it narrows on the <em>absence</em> of a field in the other members, so adding <code dir=\"ltr\">receiptId?</code> to the decline shape later would quietly break it. When you control the types, a literal discriminant (next lesson) is sturdier.</p>\n<p><code dir=\"ltr\">instanceof</code> is the tool for errors. Under <code dir=\"ltr\">strict</code>, the variable in a <code dir=\"ltr\">catch</code> clause is <code dir=\"ltr\">unknown</code>, because JavaScript lets you <code dir=\"ltr\">throw</code> anything. You narrow before you touch it:</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\"> describeFailure</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">err</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> unknown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (err </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">instanceof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Error</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\"> err.message;</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\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> err </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'string'</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\"> err;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Unknown error'</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<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>Truthiness on numbers and strings</span></p>\n<p><code dir=\"ltr\">if (!order.discount)</code>, <code dir=\"ltr\">if (!feeCents)</code> and <code dir=\"ltr\">if (!couponCode)</code> all treat a meaningful <code dir=\"ltr\">0</code> or <code dir=\"ltr\">''</code> as “missing”. TypeScript will not warn you, because both readings type-check. When the value can legitimately be zero or empty, write <code dir=\"ltr\">=== null</code> or <code dir=\"ltr\">=== undefined</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Where narrowing gets lost</h2>\n<p>Narrowing is a fact about one point in the flow. A callback runs at some other time, so TypeScript is careful about what it carries inside:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">couponCode</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> couponBanner</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\">names</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:#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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (order.couponCode </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</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\"> names.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</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\">order</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">couponCode</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toUpperCase</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} is applied`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // Error: 'order.couponCode' is possibly 'null'.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Outside the arrow function, <code dir=\"ltr\">order.couponCode</code> is <code dir=\"ltr\">string</code>. Inside it, the narrowing is gone, because <code dir=\"ltr\">couponCode</code> is a mutable property and something could set it back to <code dir=\"ltr\">null</code> before <code dir=\"ltr\">map</code> calls the callback. The fix is to capture the narrowed value in a <code dir=\"ltr\">const</code>, which can never change:</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\"> couponBanner</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\">names</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:#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\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> code</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order.couponCode;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</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\"> names.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</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\">code</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toUpperCase</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} is applied`</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>Since TypeScript 5.4, parameters and <code dir=\"ltr\">let</code> variables also keep their narrowing inside callbacks, as long as they are not assigned again after the callback is created. Properties never do. Copying into a <code dir=\"ltr\">const</code> works in every version and makes the intent obvious.</p>\n<aside class=\"callout callout-note\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>TypeScript is optimistic about function calls</span></p>\n<p>The opposite case is allowed: after <code dir=\"ltr\">if (order.couponCode !== null) { applyDiscount(order); … }</code>, TypeScript still treats <code dir=\"ltr\">order.couponCode</code> as <code dir=\"ltr\">string</code>, even though <code dir=\"ltr\">applyDiscount</code> might have changed it. Re-checking after every call would make narrowing useless, so TypeScript trusts you here. Avoid functions that mutate their arguments and this never bites.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Narrowing arrays with filter</h2>\n<p><code dir=\"ltr\">filter</code> used to be a classic frustration: <code dir=\"ltr\">coupons.filter((c) =&gt; c !== null)</code> still had type <code dir=\"ltr\">(string | null)[]</code>. Since TypeScript 5.5, TypeScript <strong>infers a type predicate</strong> from simple arrow functions like this one, so the result is <code dir=\"ltr\">string[]</code>:</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> coupons</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</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:#0A3069;--shiki-dark:#A5D6FF\">'WELCOME10'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'LOYAL5'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> codes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupons.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</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:#1F2328;--shiki-dark:#E6EDF3\"> c </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// string[]</span></span></code></pre></div>\n<p>The inference only happens when the check proves the type in both directions. <code dir=\"ltr\">(c) =&gt; !!c</code> is not inferred as a predicate: a falsy result could mean <code dir=\"ltr\">''</code>, not <code dir=\"ltr\">null</code>, so TypeScript cannot conclude that a rejected value is <code dir=\"ltr\">null</code>. The same goes for <code dir=\"ltr\">filter(Boolean)</code>. Write the explicit comparison and you get both the narrowing and the correct handling of empty strings.</p>\n<p>The exercise below has all three bugs from this lesson in one file. In the next lesson you will design types that make narrowing almost automatic: discriminated unions.</p>\n","toc":[{"id":"sec-1","text":"Control-flow analysis","level":2},{"id":"sec-2","text":"The narrowing toolbox","level":2},{"id":"sec-3","text":"in and instanceof in practice","level":3},{"id":"sec-4","text":"Where narrowing gets lost","level":2},{"id":"sec-5","text":"Narrowing arrays with filter","level":2}],"takeaways":["Control-flow analysis gives a variable a different type at each point in the code, based on the checks and returns that came before.","Truthiness checks also remove <code dir=\"ltr\">0</code> and <code dir=\"ltr\">''</code>, so check against <code dir=\"ltr\">null</code> or <code dir=\"ltr\">undefined</code> explicitly when those values are meaningful.","Narrowing of a mutable property is dropped inside callbacks; copy the value into a <code dir=\"ltr\">const</code> before the callback to keep it.","Since TypeScript 5.5, a simple arrow like <code dir=\"ltr\">(c) =&gt; c !== null</code> passed to <code dir=\"ltr\">filter</code> is inferred as a type predicate, so the result is narrowed."],"further":[{"title":"Narrowing","url":"https://www.typescriptlang.org/docs/handbook/2/narrowing.html"},{"title":"TypeScript 5.5 release notes (inferred type predicates)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html"}],"quiz":[{"q":"<p><code dir=\"ltr\">shippingFeeCents</code> is <code dir=\"ltr\">null</code> when the fee is not known yet and <code dir=\"ltr\">0</code> for free shipping. What does this return for a free-shipping order?</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\">fee</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span 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\">fee) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Calculated at checkout'</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\"> fee </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Free'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">fee</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:#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":"‘Free’","why":"The <code dir=\"ltr\">'Free'</code> branch is unreachable for <code dir=\"ltr\">0</code>, because <code dir=\"ltr\">!0</code> is <code dir=\"ltr\">true</code> and the first return fires first."},{"text":"‘Calculated at checkout’","why":"Correct. The truthiness check treats <code dir=\"ltr\">0</code> like <code dir=\"ltr\">null</code>. Write <code dir=\"ltr\">if (fee === null)</code> instead."},{"text":"‘0’","why":"The function never reaches the template string for <code dir=\"ltr\">0</code>; the truthiness check returns before that."},{"text":"It does not compile, because <code dir=\"ltr\">fee === 0</code> has no overlap after the check.","why":"After <code dir=\"ltr\">!fee</code> returns, <code dir=\"ltr\">fee</code> is still <code dir=\"ltr\">number</code>, and comparing a <code dir=\"ltr\">number</code> with <code dir=\"ltr\">0</code> is allowed. The bug is silent."}],"answer":1},{"q":"<p>Why is <code dir=\"ltr\">order.coupon</code> an error inside the callback?</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\">if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (order.coupon </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  names.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</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\">order</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupon</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toUpperCase</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></code></pre></div>\n","options":[{"text":"Because <code dir=\"ltr\">map</code> callbacks are not type-checked under <code dir=\"ltr\">strict</code>.","why":"Callbacks are fully checked. The error exists precisely because they are."},{"text":"Because TypeScript cannot know when the callback runs, and <code dir=\"ltr\">order.coupon</code> is a mutable property that could be <code dir=\"ltr\">null</code> by then.","why":"Correct. Narrowing a mutable property does not carry into a function created inside the block. Copy it into a <code dir=\"ltr\">const</code> first."},{"text":"Because <code dir=\"ltr\">!== null</code> does not narrow; you need <code dir=\"ltr\">typeof order.coupon === 'string'</code>.","why":"Comparing with <code dir=\"ltr\">null</code> narrows fine outside the callback. The problem is the callback boundary, not the check."}],"answer":1},{"q":"<p>Under TypeScript 5.9, what is the type of <code dir=\"ltr\">codes</code>?</p>","options":[{"text":"<code dir=\"ltr\">string[]</code> when written as <code dir=\"ltr\">coupons.filter((c) =&gt; c !== null)</code> with <code dir=\"ltr\">coupons: (string | null)[]</code>","why":"Correct. TypeScript 5.5 and later infer <code dir=\"ltr\">(c) =&gt; c !== null</code> as the predicate <code dir=\"ltr\">c is string</code>, so <code dir=\"ltr\">filter</code> narrows the array."},{"text":"<code dir=\"ltr\">string[]</code> when written as <code dir=\"ltr\">coupons.filter(Boolean)</code>","why":"<code dir=\"ltr\">Boolean</code> is not a type predicate, and a truthiness check cannot be one anyway because it also removes <code dir=\"ltr\">''</code>. The result stays <code dir=\"ltr\">(string | null)[]</code>."},{"text":"<code dir=\"ltr\">string[]</code> when written as <code dir=\"ltr\">coupons.filter((c) =&gt; !!c)</code>","why":"Truthiness is not inferred as a predicate, because a falsy result does not prove the value is <code dir=\"ltr\">null</code>; it might be <code dir=\"ltr\">''</code>."}],"answer":0},{"q":"<p>Which check narrows <code dir=\"ltr\">input: Order | Order[]</code> to <code dir=\"ltr\">Order[]</code>?</p>","options":[{"text":"<code dir=\"ltr\">typeof input === 'array'</code>","why":"<code dir=\"ltr\">typeof</code> never returns <code dir=\"ltr\">'array'</code>; for arrays it returns <code dir=\"ltr\">'object'</code>, and TypeScript flags the comparison."},{"text":"<code dir=\"ltr\">input instanceof Object</code>","why":"Both an <code dir=\"ltr\">Order</code> and an array are objects, so this check cannot tell them apart."},{"text":"<code dir=\"ltr\">'length' in input</code>","why":"This narrows only if <code dir=\"ltr\">Order</code> has no <code dir=\"ltr\">length</code> property, which is fragile; a dedicated check is clearer and safer."},{"text":"<code dir=\"ltr\">Array.isArray(input)</code>","why":"Correct. <code dir=\"ltr\">Array.isArray</code> is declared as a type predicate, so the true branch sees <code dir=\"ltr\">Order[]</code> and the false branch sees <code dir=\"ltr\">Order</code>."}],"answer":3}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type Order = {\n  id: number;\n  couponCode: string | null;\n  // null = not known yet (address incomplete); 0 = free shipping\n  shippingFeeCents: number | null;\n};\n\nfunction shippingLabel(order: Order): string {\n  if (!order.shippingFeeCents) return 'Calculated at checkout';\n  if (order.shippingFeeCents === 0) return 'Free';\n  return `$${(order.shippingFeeCents / 100).toFixed(2)}`;\n}\n\nfunction usedCoupons(orders: Order[]): string[] {\n  return orders.map((o) => o.couponCode).filter((c) => c);\n}\n\nfunction couponBanner(order: Order, names: string[]): string[] {\n  if (order.couponCode === null) return [];\n  return names.map((n) => `${n}, ${order.couponCode.toUpperCase()} is applied`);\n}\n\nconst orders: Order[] = [\n  { id: 1001, couponCode: 'welcome10', shippingFeeCents: 0 },\n  { id: 1002, couponCode: null, shippingFeeCents: null },\n  { id: 1003, couponCode: 'loyal5', shippingFeeCents: 499 },\n];\nconsole.log(orders.map(shippingLabel), usedCoupons(orders));\n"},"solution":{"ts":"type Order = {\n  id: number;\n  couponCode: string | null;\n  // null = not known yet (address incomplete); 0 = free shipping\n  shippingFeeCents: number | null;\n};\n\nfunction shippingLabel(order: Order): string {\n  const fee = order.shippingFeeCents;\n  if (fee === null) return 'Calculated at checkout';\n  if (fee === 0) return 'Free';\n  return `$${(fee / 100).toFixed(2)}`;\n}\n\nfunction usedCoupons(orders: Order[]): string[] {\n  return orders.map((o) => o.couponCode).filter((c) => c !== null);\n}\n\nfunction couponBanner(order: Order, names: string[]): string[] {\n  const code = order.couponCode;\n  if (code === null) return [];\n  return names.map((n) => `${n}, ${code.toUpperCase()} is applied`);\n}\n\nconst orders: Order[] = [\n  { id: 1001, couponCode: 'welcome10', shippingFeeCents: 0 },\n  { id: 1002, couponCode: null, shippingFeeCents: null },\n  { id: 1003, couponCode: 'loyal5', shippingFeeCents: 499 },\n];\nconsole.log(orders.map(shippingLabel), usedCoupons(orders));\n"},"checks":[{"id":"types","typecheck":true},{"id":"free-shipping","test":"const o = (fee) => ({ id: 1, couponCode: null, shippingFeeCents: fee }); return shippingLabel(o(0)) === 'Free' && shippingLabel(o(null)) === 'Calculated at checkout' && shippingLabel(o(499)) === '$4.99';"},{"id":"used-coupons","test":"const r = usedCoupons([{ id: 1, couponCode: 'A', shippingFeeCents: 0 }, { id: 2, couponCode: null, shippingFeeCents: 0 }, { id: 3, couponCode: 'B', shippingFeeCents: 0 }]); return JSON.stringify(r) === '[\"A\",\"B\"]';"},{"id":"banner","test":"const r = couponBanner({ id: 1, couponCode: 'loyal5', shippingFeeCents: 0 }, ['Mona']); return r.length === 1 && r[0].includes('LOYAL5') && couponBanner({ id: 2, couponCode: null, shippingFeeCents: 0 }, ['Mona']).length === 0;"}],"title":"Fix three narrowing bugs","prompt":"<p>This file has the three narrowing problems from the lesson. Fix them without using the non-null assertion <code dir=\"ltr\">!</code>, <code dir=\"ltr\">as</code> or <code dir=\"ltr\">any</code>:</p>\n<ol>\n<li><code dir=\"ltr\">shippingLabel</code> must return <code dir=\"ltr\">'Free'</code> when <code dir=\"ltr\">shippingFeeCents</code> is <code dir=\"ltr\">0</code>, <code dir=\"ltr\">'Calculated at checkout'</code> when it is <code dir=\"ltr\">null</code>, and a dollar amount like <code dir=\"ltr\">'$4.99'</code> otherwise.</li>\n<li><code dir=\"ltr\">usedCoupons</code> must return a real <code dir=\"ltr\">string[]</code> of the non-null coupon codes.</li>\n<li><code dir=\"ltr\">couponBanner</code> must compile and still upper-case the code inside the callback.</li>\n</ol>\n","hints":["For 1, compare against <code dir=\"ltr\">null</code> instead of relying on truthiness.","For 2, TypeScript 5.5+ infers a type predicate from <code dir=\"ltr\">(c) =&gt; c !== null</code>, but not from <code dir=\"ltr\">(c) =&gt; c</code>.","For 3, copy <code dir=\"ltr\">order.couponCode</code> into a <code dir=\"ltr\">const</code> before you check it, and use that <code dir=\"ltr\">const</code> inside the arrow function."],"explanation":"<p><code dir=\"ltr\">!fee</code> is true for <code dir=\"ltr\">0</code>, so free shipping fell into the <code dir=\"ltr\">null</code> branch; checking <code dir=\"ltr\">=== null</code> keeps the two meanings apart. <code dir=\"ltr\">filter((c) =&gt; c !== null)</code> is inferred as the predicate <code dir=\"ltr\">c is string</code>, so the array is narrowed to <code dir=\"ltr\">string[]</code>, while a truthiness callback is not a predicate. In <code dir=\"ltr\">couponBanner</code>, the narrowing of the mutable property <code dir=\"ltr\">order.couponCode</code> does not survive into the callback, but a <code dir=\"ltr\">const</code> can never change, so its narrowed type does.</p>\n","checkLabels":{"types":"The file type-checks with zero errors","free-shipping":"<code dir=\"ltr\">shippingLabel</code> returns ‘Free’ for 0, ‘Calculated at checkout’ for null and ‘$4.99’ for 499","used-coupons":"<code dir=\"ltr\">usedCoupons</code> keeps only the non-null codes, in order","banner":"<code dir=\"ltr\">couponBanner</code> upper-cases the code and returns [] when there is no coupon"}},"words":744,"readMinutes":4}