{"id":"l-at-1-5","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Stop literals from widening into string, derive union types from runtime arrays with as const, and validate config objects with satisfies without losing their precise types.","html":"<p>Here is a bug report that every TypeScript team has filed at least once: “I’m passing exactly the right string and it says <code dir=\"ltr\">Type 'string' is not assignable to type 'OrderStatus'</code>.”</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'processing'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'returned'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span 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\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> save</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>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> draft</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\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, status: </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:#8250DF;--shiki-dark:#D2A8FF\">save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type '{ id: number; status: string; }' is not assignable</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// to parameter of type 'Order'.</span></span></code></pre></div>\n<p>The string <em>is</em> <code dir=\"ltr\">'processing'</code>. But TypeScript did not infer <code dir=\"ltr\">'processing'</code> for it. It inferred <code dir=\"ltr\">string</code>. This is <strong>widening</strong>, and understanding it explains three tools you will use every week: literal types, <code dir=\"ltr\">as const</code> and <code dir=\"ltr\">satisfies</code>.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Why literals widen</h2>\n<p>TypeScript infers the type a value is likely to have <em>for its whole life</em>, not just the value it starts with. A <code dir=\"ltr\">const</code> can never change, so it keeps the literal. A <code dir=\"ltr\">let</code> or an object property can be reassigned, so TypeScript widens it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> a</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:#6E7781;--shiki-dark:#8B949E\">// 'processing'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> b </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:#6E7781;--shiki-dark:#8B949E\">// string</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> c</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'processing'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// { status: string }</span></span></code></pre></div>\n<p>The last one is the surprise. <code dir=\"ltr\">const c</code> stops you reassigning <code dir=\"ltr\">c</code>, but <code dir=\"ltr\">c.status = 'anything'</code> is still legal JavaScript, so the property widens to <code dir=\"ltr\">string</code>.</p>\n<p>There are three good fixes, in order of preference. Write the literal where the type is already known (<code dir=\"ltr\">save({ id: 1001, status: 'processing' })</code>), so it is checked against <code dir=\"ltr\">Order</code> directly. Annotate the variable (<code dir=\"ltr\">const draft: Order = …</code>) when you build it ahead of time. Or use <code dir=\"ltr\">as const</code>, which comes next.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">as const: freeze the literals</h2>\n<p><code dir=\"ltr\">as const</code> tells TypeScript to infer the narrowest possible type: every literal stays a literal, every array becomes a readonly tuple, every property becomes <code dir=\"ltr\">readonly</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNTRIES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'EG'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'AE'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'SA'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'JO'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'GB'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'DE'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'US'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'CA'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// readonly ['EG', 'AE', 'SA', 'JO', 'GB', 'DE', 'US', 'CA']</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\"> Country</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNTRIES</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 'EG' | 'AE' | 'SA' | 'JO' | 'GB' | 'DE' | 'US' | 'CA'</span></span></code></pre></div>\n<p>That second line is one of the most useful patterns in everyday TypeScript. <code dir=\"ltr\">typeof COUNTRIES</code> turns the value into its type, and <code dir=\"ltr\">[number]</code> asks “what type do I get when I index this with any number?” (you will meet both operators properly in <a href=\"/lesson/advanced-typescript/l-at-3-1\">keyof, typeof and Indexed Access Types</a>). The result is a union that is <strong>derived from a runtime array</strong>. You get one source of truth: the array exists at runtime for dropdowns and validation, and the type follows it automatically.</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\"> isCountry</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 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\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> is</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Country</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\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">COUNTRIES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> readonly</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">includes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(value);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The widening cast inside <code dir=\"ltr\">isCountry</code> is needed because <code dir=\"ltr\">includes</code> on a tuple of literals only accepts those literals, which defeats the point of a check on an arbitrary string.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Prefer literal unions to enums</span></p>\n<p><code dir=\"ltr\">enum OrderStatus { Processing = 'processing', … }</code> gives you a similar union, but enums generate runtime code, are not compatible with plain string values from JSON without a cast, and are not supported by type stripping (Node’s built-in TypeScript support rejects them, and so does the <code dir=\"ltr\">erasableSyntaxOnly</code> flag). An <code dir=\"ltr\">as const</code> array or object plus a derived union does the same job in plain JavaScript.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Annotation versus satisfies</h2>\n<p>Cartwheel’s coupons live in a config object. The obvious way to type it is an annotation:</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\"> Coupon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">kind</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">percentOff</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\">kind</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'free-shipping'</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> coupons</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">Coupon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  WELCOME10: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, percentOff: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  FREESHIP: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'free-shipping'</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:#1F2328;--shiki-dark:#E6EDF3\">coupons.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">WELCOME10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.percentOff; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Property 'percentOff' does not exist on type 'Coupon'.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupons.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">SPRNG15</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;              </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// compiles, undefined at runtime</span></span></code></pre></div>\n<p>The annotation <em>replaces</em> what TypeScript knew. You wrote <code dir=\"ltr\">WELCOME10</code> as a percent coupon, but its type is now just <code dir=\"ltr\">Coupon</code>, and the object accepts any key at all, typos included.</p>\n<p><code dir=\"ltr\">satisfies</code> checks the value against a type <strong>without replacing the inferred type</strong>:</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\">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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  WELCOME10: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, percentOff: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  FREESHIP: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'free-shipping'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">} </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">satisfies</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">Coupon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupons.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">WELCOME10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.percentOff; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// number</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coupons.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">SPRNG15</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;              </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Property 'SPRNG15' does not exist on type …</span></span></code></pre></div>\n<p>You still get validation: a misspelled <code dir=\"ltr\">percentof</code> or a <code dir=\"ltr\">kind: 'bogo'</code> is flagged right in the object literal. And you keep the precise keys and variants. <code dir=\"ltr\">satisfies</code> also contextually types the literals, so <code dir=\"ltr\">kind</code> stays <code dir=\"ltr\">'percent'</code> instead of widening to <code dir=\"ltr\">string</code>, without needing <code dir=\"ltr\">as const</code>.</p>\n<p>Use the combination <code dir=\"ltr\">as const satisfies T</code> when you want both: deeply readonly literals and a check against a shape. It reads left to right as “freeze this, then make sure it fits”.</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>Using as to make an error go away</span></p>\n<p><code dir=\"ltr\">const config = { retries: 3 } as CheckoutConfig</code> compiles even when <code dir=\"ltr\">CheckoutConfig</code> requires <code dir=\"ltr\">timeoutMs</code>, because an assertion only checks that the types overlap. It is you overruling the compiler. Reserve <code dir=\"ltr\">as</code> for the rare case where you know more than TypeScript (and leave a comment saying why). When you want a check, use an annotation or <code dir=\"ltr\">satisfies</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Choosing between them</h2>\n<p>A simple default: <strong>annotate</strong> function parameters, return types and variables whose type should be the general one (<code dir=\"ltr\">let current: Order</code>). Use <strong><code dir=\"ltr\">satisfies</code></strong> for config objects, lookup tables and anything where the specific keys and values matter later. Use <strong><code dir=\"ltr\">as const</code></strong> for fixed lists you want as a union. All three are compile-time only; none of them changes a byte of the emitted JavaScript.</p>\n<p>In the exercise you will derive <code dir=\"ltr\">Country</code> from Cartwheel’s country list and type the currency table so that it rejects unknown countries and remembers exact currencies. That completes the design toolkit for this section; next you move on to generics, starting with the relationships they express.</p>\n","toc":[{"id":"sec-1","text":"Why literals widen","level":2},{"id":"sec-2","text":"as const: freeze the literals","level":2},{"id":"sec-3","text":"Annotation versus satisfies","level":2},{"id":"sec-4","text":"Choosing between them","level":2}],"takeaways":["<code dir=\"ltr\">let</code> variables and object properties widen literals to <code dir=\"ltr\">string</code> or <code dir=\"ltr\">number</code>; <code dir=\"ltr\">const</code> variables keep the literal.","<code dir=\"ltr\">as const</code> makes a value deeply readonly and keeps every literal, which lets you derive a union type from a runtime array.","A type annotation replaces the inferred type with the annotated one; <code dir=\"ltr\">satisfies</code> checks against a type and keeps the inferred one.","<code dir=\"ltr\">as</code> assertions skip checks such as missing properties, so use <code dir=\"ltr\">satisfies</code> when you want validation rather than trust."],"further":[{"title":"The satisfies operator (TypeScript 4.9 release notes)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html"},{"title":"Literal types","url":"https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types"},{"title":"const assertions (TypeScript 3.4 release notes)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html"}],"quiz":[{"q":"<p>Why does <code dir=\"ltr\">save(draft)</code> fail when <code dir=\"ltr\">Order['status']</code> is <code dir=\"ltr\">'processing' | 'shipped'</code>?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> draft</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\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, status: </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:#8250DF;--shiki-dark:#D2A8FF\">save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft);</span></span></code></pre></div>\n","options":[{"text":"Because <code dir=\"ltr\">const</code> makes the whole object readonly, and <code dir=\"ltr\">save</code> expects a mutable one.","why":"<code dir=\"ltr\">const</code> only stops reassigning <code dir=\"ltr\">draft</code>. Its properties stay mutable, which is exactly why they widen."},{"text":"Because <code dir=\"ltr\">draft.status</code> is inferred as <code dir=\"ltr\">string</code>; object properties widen since they could be reassigned later.","why":"Correct. Write the literal where the type is known (<code dir=\"ltr\">save({ … })</code>), annotate <code dir=\"ltr\">draft</code> as <code dir=\"ltr\">Order</code>, or use <code dir=\"ltr\">as const</code>."},{"text":"Because <code dir=\"ltr\">draft</code> has no <code dir=\"ltr\">customerId</code>.","why":"The <code dir=\"ltr\">Order</code> in the question only needs <code dir=\"ltr\">id</code> and <code dir=\"ltr\">status</code>. The failure is about the widened <code dir=\"ltr\">status</code>."}],"answer":1},{"q":"<p>What is <code dir=\"ltr\">Country</code> here?</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNTRIES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'EG'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'AE'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'GB'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</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\"> Country</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNTRIES</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">string</code>","why":"That would be the result without <code dir=\"ltr\">as const</code>, where the array is inferred as <code dir=\"ltr\">string[]</code>."},{"text":"<code dir=\"ltr\">readonly ['EG', 'AE', 'GB']</code>","why":"That is <code dir=\"ltr\">typeof COUNTRIES</code>. Indexing it with <code dir=\"ltr\">[number]</code> gives the type of its elements."},{"text":"<code dir=\"ltr\">'EG' | 'AE' | 'GB'</code>","why":"Correct. <code dir=\"ltr\">as const</code> keeps the literals, and <code dir=\"ltr\">[number]</code> asks for the type of any element of the tuple."}],"answer":2},{"q":"<p><code dir=\"ltr\">coupons</code> is declared with <code dir=\"ltr\">satisfies Record&lt;string, Coupon&gt;</code>. Which line is an error?</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\"> Coupon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">kind</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">percentOff</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\">kind</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'free-shipping'</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\"> coupons</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  WELCOME10: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'percent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, percentOff: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  FREESHIP: { kind: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'free-shipping'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">} </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">satisfies</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">Coupon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">coupons.WELCOME10.percentOff</code>","why":"<code dir=\"ltr\">satisfies</code> keeps the inferred type, so TypeScript knows <code dir=\"ltr\">WELCOME10</code> is the percent variant and <code dir=\"ltr\">percentOff</code> exists."},{"text":"<code dir=\"ltr\">coupons.FREESHIP.kind === 'free-shipping'</code>","why":"The inferred type of <code dir=\"ltr\">kind</code> is the literal <code dir=\"ltr\">'free-shipping'</code>, so this comparison is valid (and always true)."},{"text":"<code dir=\"ltr\">coupons.SPRING15</code>","why":"Correct. The inferred type has only the two keys you wrote, so reading an unknown key is an error, unlike with a <code dir=\"ltr\">Record&lt;string, Coupon&gt;</code> annotation."}],"answer":2},{"q":"<p>A teammate writes <code dir=\"ltr\">const config = { retries: 3 } as CheckoutConfig;</code>, and <code dir=\"ltr\">CheckoutConfig</code> also requires <code dir=\"ltr\">timeoutMs</code>. What happens?</p>","options":[{"text":"It compiles, because an assertion only needs the types to overlap; the missing <code dir=\"ltr\">timeoutMs</code> is never reported.","why":"Correct. <code dir=\"ltr\">as</code> tells the compiler to trust you. Use <code dir=\"ltr\">satisfies CheckoutConfig</code> or an annotation to get the missing-property error."},{"text":"It fails with “Property ‘timeoutMs’ is missing”.","why":"That error comes from annotations and <code dir=\"ltr\">satisfies</code>. An assertion only rejects types that do not overlap at all."},{"text":"It adds <code dir=\"ltr\">timeoutMs: undefined</code> to the object at runtime.","why":"Type assertions are erased completely; they never add or change values."}],"answer":0}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type Currency = 'EGP' | 'AED' | 'SAR' | 'JOD' | 'GBP' | 'EUR' | 'USD' | 'CAD';\ntype Coupon = { kind: 'percent'; percentOff: number } | { kind: 'free-shipping' };\n\nconst COUNTRIES = ['EG', 'AE', 'SA', 'JO', 'GB', 'DE', 'US', 'CA'];\ntype Country = string; // derive this from COUNTRIES\n\nconst currencyByCountry: Record<string, Currency> = {\n  EG: 'EGP',\n  AE: 'AED',\n  SA: 'SAR',\n  JO: 'JOD',\n  GB: 'GBP',\n  DE: 'EUR',\n  US: 'USD',\n  CA: 'CAD',\n};\n\nconst coupons: Record<string, Coupon> = {\n  WELCOME10: { kind: 'percent', percentOff: 10 },\n  SPRING15: { kind: 'percent', percentOff: 15 },\n  FREESHIP: { kind: 'free-shipping' },\n};\n\nfunction isCountry(value: string): value is Country {\n  return COUNTRIES.includes(value);\n}\n\nfunction currencyFor(country: Country): Currency {\n  return currencyByCountry[country];\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<Country, 'EG' | 'AE' | 'SA' | 'JO' | 'GB' | 'DE' | 'US' | 'CA'>>;\ntype T2 = Expect<Equal<(typeof currencyByCountry)['DE'], 'EUR'>>;\ntype T3 = Expect<Equal<keyof typeof coupons, 'WELCOME10' | 'SPRING15' | 'FREESHIP'>>;\ntype T4 = Expect<Equal<(typeof coupons)['SPRING15']['percentOff'], number>>;\n\n// @ts-expect-error: France is not a Cartwheel country\ncurrencyByCountry.FR;\n\n// @ts-expect-error: typo in the coupon code\ncoupons.SPRNG15;\n\nconsole.log(currencyFor('DE'), isCountry('EG'), isCountry('FR'));\n"},"solution":{"ts":"type Currency = 'EGP' | 'AED' | 'SAR' | 'JOD' | 'GBP' | 'EUR' | 'USD' | 'CAD';\ntype Coupon = { kind: 'percent'; percentOff: number } | { kind: 'free-shipping' };\n\nconst COUNTRIES = ['EG', 'AE', 'SA', 'JO', 'GB', 'DE', 'US', 'CA'] as const;\ntype Country = (typeof COUNTRIES)[number];\n\nconst currencyByCountry = {\n  EG: 'EGP',\n  AE: 'AED',\n  SA: 'SAR',\n  JO: 'JOD',\n  GB: 'GBP',\n  DE: 'EUR',\n  US: 'USD',\n  CA: 'CAD',\n} as const satisfies Record<Country, Currency>;\n\nconst coupons = {\n  WELCOME10: { kind: 'percent', percentOff: 10 },\n  SPRING15: { kind: 'percent', percentOff: 15 },\n  FREESHIP: { kind: 'free-shipping' },\n} satisfies Record<string, Coupon>;\n\nfunction isCountry(value: string): value is Country {\n  return (COUNTRIES as readonly string[]).includes(value);\n}\n\nfunction currencyFor(country: Country): Currency {\n  return currencyByCountry[country];\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<Country, 'EG' | 'AE' | 'SA' | 'JO' | 'GB' | 'DE' | 'US' | 'CA'>>;\ntype T2 = Expect<Equal<(typeof currencyByCountry)['DE'], 'EUR'>>;\ntype T3 = Expect<Equal<keyof typeof coupons, 'WELCOME10' | 'SPRING15' | 'FREESHIP'>>;\ntype T4 = Expect<Equal<(typeof coupons)['SPRING15']['percentOff'], number>>;\n\n// @ts-expect-error: France is not a Cartwheel country\ncurrencyByCountry.FR;\n\n// @ts-expect-error: typo in the coupon code\ncoupons.SPRNG15;\n\nconsole.log(currencyFor('DE'), isCountry('EG'), isCountry('FR'));\n"},"checks":[{"id":"types","typecheck":true},{"id":"is-country","test":"return isCountry('EG') === true && isCountry('CA') === true && isCountry('FR') === false;"},{"id":"currency","test":"return currencyFor('DE') === 'EUR' && currencyFor('JO') === 'JOD';"}],"title":"Derive countries, check the tables","prompt":"<p>Make Cartwheel’s lookup tables precise without repeating yourself:</p>\n<ol>\n<li>Derive <code dir=\"ltr\">Country</code> from the <code dir=\"ltr\">COUNTRIES</code> array, so it is the union <code dir=\"ltr\">'EG' | 'AE' | … | 'CA'</code>.</li>\n<li>Type <code dir=\"ltr\">currencyByCountry</code> so it must have one entry per <code dir=\"ltr\">Country</code>, and so <code dir=\"ltr\">currencyByCountry.DE</code> has the exact type <code dir=\"ltr\">'EUR'</code>.</li>\n<li>Type <code dir=\"ltr\">coupons</code> so every value is checked against <code dir=\"ltr\">Coupon</code> but the object only has the three keys you wrote, and <code dir=\"ltr\">SPRING15</code> is known to be a percent coupon.</li>\n</ol>\n<p>Keep <code dir=\"ltr\">isCountry</code> working at runtime for any string.</p>\n","hints":["<code dir=\"ltr\">as const</code> on the array keeps its literals; <code dir=\"ltr\">(typeof COUNTRIES)[number]</code> turns them into a union.","Replace the annotations on the two objects with <code dir=\"ltr\">satisfies</code>: <code dir=\"ltr\">{ … } satisfies Record&lt;Country, Currency&gt;</code>. Because <code dir=\"ltr\">Currency</code> is a union of literals, <code dir=\"ltr\">satisfies</code> keeps <code dir=\"ltr\">'EUR'</code> literal on its own; adding <code dir=\"ltr\">as const</code> in front also makes the table readonly.","Once <code dir=\"ltr\">COUNTRIES</code> is a readonly tuple, <code dir=\"ltr\">includes</code> only accepts countries. Widen it for the check: <code dir=\"ltr\">(COUNTRIES as readonly string[]).includes(value)</code>."],"explanation":"<p><code dir=\"ltr\">as const</code> freezes the array into a readonly tuple of literals, and indexing its type with <code dir=\"ltr\">[number]</code> gives the union of its elements: one list drives both the runtime check and the type. The annotations on the two tables replaced their inferred types with general ones (any string key, <code dir=\"ltr\">Currency</code> values, <code dir=\"ltr\">Coupon</code> variants). <code dir=\"ltr\">satisfies</code> keeps the check (a missing country, a misspelled <code dir=\"ltr\">percentOff</code> or an unknown currency would still be flagged) while preserving exact keys and values. Because the contextual type <code dir=\"ltr\">Currency</code> is a union of literals, <code dir=\"ltr\">'EUR'</code> stays a literal instead of widening to <code dir=\"ltr\">string</code>; <code dir=\"ltr\">as const satisfies …</code> would additionally make the table readonly.</p>\n","checkLabels":{"types":"The file type-checks with zero errors, including all type tests","is-country":"<code dir=\"ltr\">isCountry</code> accepts Cartwheel countries and rejects others at runtime","currency":"<code dir=\"ltr\">currencyFor</code> returns the right currency"}},"words":725,"readMinutes":3}