{"id":"l-at-1-2","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Predict when TypeScript accepts one type where another is expected, including excess property checks and function parameters, so assignability errors stop surprising you.","html":"<p>Here is a function from Cartwheel’s checkout client and a call that surprises people coming from Java or C#:</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\"> Money</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">amountCents</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\">currency</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\"> formatMoney</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">m</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</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\">  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\">m</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">amountCents</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\">m</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">currency</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { sku: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'TENT-2P'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Two-person tent'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">18900</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, currency: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'USD'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(tent); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// compiles: \"189.00 USD\"</span></span></code></pre></div>\n<p><code dir=\"ltr\">tent</code> was never declared as <code dir=\"ltr\">Money</code>. It is a product. TypeScript accepts it anyway, because TypeScript does not ask what something is called. It asks whether it has the right shape. This is <strong>structural typing</strong>, and once you see it clearly, most “why does this compile?” moments go away.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Types are sets of values</h2>\n<p>The most useful mental model for this whole course: a type is a set of values. <code dir=\"ltr\">string</code> is the set of all strings. <code dir=\"ltr\">'USD'</code> is the set with one value in it. <code dir=\"ltr\">'USD' | 'EUR'</code> has two.</p>\n<p><strong>Assignability is the subset relation.</strong> A value of type A can go where B is expected when every A is also a B. Every <code dir=\"ltr\">'USD'</code> is a string, so <code dir=\"ltr\">'USD'</code> is assignable to <code dir=\"ltr\">string</code>. Not every string is <code dir=\"ltr\">'USD'</code>, so the reverse needs a check.</p>\n<p>Object types work the same way, with a twist that trips people up. <code dir=\"ltr\">{ id: number }</code> is the set of all values that have a numeric <code dir=\"ltr\">id</code>. <code dir=\"ltr\">{ id: number; status: string }</code> adds a second condition, so fewer values qualify. <strong>More properties means a smaller set.</strong> That is why a full <code dir=\"ltr\">Order</code> can be passed where <code dir=\"ltr\">{ id: number }</code> is expected: it is in the smaller set, so it is in the bigger one too.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 280\" role=\"img\" aria-labelledby=\"l-at-1-2-t1\"><defs><marker id=\"arrow-l-at-1-2-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-2-t1\">Two nested-set diagrams. Left: unknown contains string, which contains the union of USD and EUR, which contains USD. Right: an object with id contains an object with id and status, which contains a full Order. Every value in an inner set also belongs to the sets around it.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"20\" width=\"320\" height=\"240\" rx=\"14\"/>\n  <text class=\"d-label-muted\" x=\"26\" y=\"46\">unknown</text>\n  <rect class=\"d-box-accent\" x=\"40\" y=\"60\" width=\"260\" height=\"180\" rx=\"12\"/>\n  <text class=\"d-label\" x=\"56\" y=\"86\">string</text>\n  <rect class=\"d-box-primary\" x=\"70\" y=\"100\" width=\"200\" height=\"120\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"86\" y=\"126\">'USD' | 'EUR'</text>\n  <rect class=\"d-box-success\" x=\"100\" y=\"146\" width=\"140\" height=\"56\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"170\" y=\"179\" text-anchor=\"middle\">'USD'</text>\n  <rect class=\"d-box\" x=\"350\" y=\"20\" width=\"320\" height=\"240\" rx=\"14\"/>\n  <text class=\"d-code\" x=\"366\" y=\"46\">{ id }</text>\n  <rect class=\"d-box-accent\" x=\"380\" y=\"60\" width=\"260\" height=\"180\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"396\" y=\"86\">{ id, status }</text>\n  <rect class=\"d-box-success\" x=\"410\" y=\"100\" width=\"200\" height=\"120\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"510\" y=\"150\" text-anchor=\"middle\">Order</text>\n  <text class=\"d-label-muted\" x=\"510\" y=\"176\" text-anchor=\"middle\">id, status, total…</text>\n</svg><figcaption>Assignability is the subset relation: narrower types fit inside wider ones</figcaption></figure>\n<p>Two special types sit at the edges. <code dir=\"ltr\">unknown</code> is the set of every value: anything is assignable to it, and you must check before using it. <code dir=\"ltr\">never</code> is the empty set: it is assignable to everything (the empty set is a subset of every set), and nothing is assignable to it. You will lean on both later in the course. <code dir=\"ltr\">any</code> is not a set at all; it switches checking off in both directions, which is why one stray <code dir=\"ltr\">any</code> can quietly poison everything it touches.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Excess property checks: the one exception</h2>\n<p>If extra properties are fine, why does this fail?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> 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:#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\">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:#8250DF;--shiki-dark:#D2A8FF\">save</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\">, stauts: </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:#6E7781;--shiki-dark:#8B949E\">// Error: Object literal may only specify known properties, but 'stauts'</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// does not exist in type 'Order'. Did you mean to write 'status'?</span></span></code></pre></div>\n<p>This is an <strong>excess property check</strong>, a deliberate heuristic layered on top of structural typing. When you write an object literal directly where a type is expected, the literal is “fresh”: nothing else can ever see it, so an extra property can only be a typo. TypeScript flags it.</p>\n<p>The check only applies to fresh literals:</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\"> 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\">, stauts: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'oops'</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 style=\"color:#6E7781;--shiki-dark:#8B949E\">// compiles: draft is not fresh</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>Trusting excess checks to protect you</span></p>\n<p>Excess property checks are a typo detector, not a guarantee. The moment a value passes through a variable, a function return, or an <code dir=\"ltr\">as</code> assertion, extra properties sail through. If a function must not receive certain fields (say, a card number going to a logger), do not rely on this check; pick the fields you need explicitly.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Functions: parameters flip the direction</h2>\n<p>For functions, return types follow the usual rule, but parameters go the other way. A handler that accepts <em>any</em> <code dir=\"ltr\">CheckoutEvent</code> can stand in for one that only needs <code dir=\"ltr\">PaymentFailed</code>. The reverse is unsafe: a handler that only understands <code dir=\"ltr\">PaymentFailed</code> would crash on a <code dir=\"ltr\">shipped</code> event. Under <code dir=\"ltr\">strict</code> (specifically <code dir=\"ltr\">strictFunctionTypes</code>), TypeScript rejects 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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentFailed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'payment-failed'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">reason</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderShipped</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'order-shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">carrier</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentFailed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderShipped</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> failuresOnly</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentFailed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.reason.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toUpperCase</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\"> l</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> failuresOnly;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Type '{ onEvent: (e: PaymentFailed) => void; }' is not assignable to type 'Listener'.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">//   Types of property 'onEvent' are incompatible.</span></span></code></pre></div>\n<p>Fewer parameters are also fine: <code dir=\"ltr\">(e) =&gt; {}</code> and <code dir=\"ltr\">() =&gt; {}</code> both fit <code dir=\"ltr\">(e: CheckoutEvent, at: Date) =&gt; void</code>, because a function is allowed to ignore arguments. That is why <code dir=\"ltr\">orders.forEach((o) =&gt; …)</code> works even though <code dir=\"ltr\">forEach</code> passes three arguments.</p>\n<h3 id=\"sec-4\" tabindex=\"-1\">The method-syntax loophole</h3>\n<p>Now write the same type with method syntax:</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\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  onEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// method syntax</span></span>\n<span class=\"line\"><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\"> l</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> failuresOnly; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// compiles!</span></span></code></pre></div>\n<p>Parameters of methods declared this way are checked <strong>bivariantly</strong>, even under <code dir=\"ltr\">strict</code>. TypeScript keeps this on purpose so that everyday patterns, such as treating an <code dir=\"ltr\">Array&lt;Dog&gt;</code> as an <code dir=\"ltr\">Array&lt;Animal&gt;</code>, keep compiling. The cost is that the unsafe listener above is accepted.</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>Use property syntax for callbacks</span></p>\n<p>When a type describes a callback or handler, write it as a property with a function type: <code dir=\"ltr\">onEvent: (e: CheckoutEvent) =&gt; void</code>. You get full parameter checking for free. Keep method syntax for classes and objects where the method really is a method.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">When you want names to matter</h2>\n<p>Sometimes structural typing is too generous. An <code dir=\"ltr\">OrderId</code> and a <code dir=\"ltr\">CustomerId</code> are both <code dir=\"ltr\">number</code>, so nothing stops you swapping them. The fix is to make the shapes differ, which is what branded types do in <a href=\"/lesson/advanced-typescript/l-at-4-1\">Branded Types and Exhaustive Checks</a>. For now, the rule of thumb is enough: if two things have the same shape, TypeScript considers them the same thing.</p>\n<p>The exercise below fixes the listener loophole in Cartwheel’s event bus. Next, you will see how TypeScript narrows a wide type inside a function, the other half of working with sets.</p>\n","toc":[{"id":"sec-1","text":"Types are sets of values","level":2},{"id":"sec-2","text":"Excess property checks: the one exception","level":2},{"id":"sec-3","text":"Functions: parameters flip the direction","level":2},{"id":"sec-4","text":"The method-syntax loophole","level":3},{"id":"sec-5","text":"When you want names to matter","level":2}],"takeaways":["TypeScript compares shapes, not names; any value with the required properties is assignable, whatever its type is called.","Read a type as the set of values it allows; an object type with more required properties describes a smaller set.","Excess property checks only apply to fresh object literals, which is why passing a variable with an extra field compiles.","Function parameters are checked in the opposite direction, but methods declared with method syntax are checked bivariantly, so prefer property syntax for callbacks."],"further":[{"title":"Type Compatibility","url":"https://www.typescriptlang.org/docs/handbook/type-compatibility.html"},{"title":"strictFunctionTypes","url":"https://www.typescriptlang.org/tsconfig/strictFunctionTypes.html"}],"quiz":[{"q":"<p>Does this compile?</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\"> Money</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">amountCents</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\">currency</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\"> product</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { sku: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'TENT-2P'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">18900</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, currency: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'USD'</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\"> price</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Money</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> product;</span></span></code></pre></div>\n","options":[{"text":"Yes, because <code dir=\"ltr\">product</code> has every property <code dir=\"ltr\">Money</code> requires and extra properties are allowed on a non-fresh value.","why":"Correct. <code dir=\"ltr\">product</code> is a variable, not a fresh literal, so the excess <code dir=\"ltr\">sku</code> is not checked; the shape matches."},{"text":"No, because <code dir=\"ltr\">product</code> was never declared as a <code dir=\"ltr\">Money</code>.","why":"TypeScript is structural; it never asks what a value was declared as, only whether its shape fits."},{"text":"No, because <code dir=\"ltr\">sku</code> is an excess property.","why":"Excess property checks only run on object literals written directly where the type is expected. Through a variable, extra fields are fine."}],"answer":0},{"q":"<p>Thinking of types as sets of values, which statement is true?</p>","options":[{"text":"<code dir=\"ltr\">{ id: number; status: string }</code> is a bigger set than <code dir=\"ltr\">{ id: number }</code>.","why":"Each required property is an extra condition a value must meet, so adding one shrinks the set, not grows it."},{"text":"<code dir=\"ltr\">never</code> contains every value, which is why it is assignable to everything.","why":"<code dir=\"ltr\">never</code> is the empty set. It is assignable to everything because the empty set is a subset of every set."},{"text":"<code dir=\"ltr\">'EUR'</code> is a subset of <code dir=\"ltr\">string</code>, so a <code dir=\"ltr\">'EUR'</code> is assignable to <code dir=\"ltr\">string</code> but not the reverse.","why":"Correct. Assignability is the subset relation; a wider set cannot flow into a narrower one without a check."},{"text":"<code dir=\"ltr\">unknown</code> and <code dir=\"ltr\">any</code> are both the empty set.","why":"<code dir=\"ltr\">unknown</code> is the set of all values. <code dir=\"ltr\">any</code> is not a set at all; it turns checking off in both directions."}],"answer":2},{"q":"<p>Why does this compile under <code dir=\"ltr\">strict</code>?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'paid'</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\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">carrier</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\">interface</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> l</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Listener</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onEvent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">carrier</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:#1F2328;--shiki-dark:#E6EDF3\"> {} };</span></span></code></pre></div>\n","options":[{"text":"Because arrow functions are never checked against interfaces.","why":"Arrow functions are checked like any other function. The leniency comes from how <code dir=\"ltr\">onEvent</code> is declared."},{"text":"Because <code dir=\"ltr\">onEvent</code> uses method syntax, and method parameters are checked bivariantly even with <code dir=\"ltr\">strictFunctionTypes</code>.","why":"Correct. Writing <code dir=\"ltr\">onEvent: (e: CheckoutEvent) =&gt; void</code> instead makes the parameter check strict and rejects this listener."},{"text":"Because a union parameter accepts any of its members.","why":"The listener must handle every member of the union. Accepting only one member is unsafe, which strict checking would catch."}],"answer":1},{"q":"<p>Which call is rejected by an excess property check?</p>","options":[{"text":"<code dir=\"ltr\">ship(order)</code> where <code dir=\"ltr\">order</code> is a variable with an extra <code dir=\"ltr\">notes</code> field","why":"Variables are not fresh, so their extra properties are not checked."},{"text":"<code dir=\"ltr\">ship({ ...order, notes: 'gift' })</code> where <code dir=\"ltr\">Order</code> has no <code dir=\"ltr\">notes</code> field","why":"Correct. A literal with a spread is still a fresh object literal, and <code dir=\"ltr\">notes</code> is written directly in it, so it is flagged. (Extra fields that arrive through <code dir=\"ltr\">...order</code> itself are not checked.)"},{"text":"<code dir=\"ltr\">ship(order as Order)</code> where <code dir=\"ltr\">order</code> has an extra <code dir=\"ltr\">notes</code> field","why":"A type assertion tells the compiler to trust you, which skips the excess check entirely."}],"answer":1}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type PaymentFailed = { type: 'payment-failed'; orderId: number; reason: string };\ntype OrderShipped = { type: 'order-shipped'; orderId: number; carrier: string };\ntype CheckoutEvent = PaymentFailed | OrderShipped;\n\n// Cartwheel's checkout event bus.\ninterface CheckoutListener {\n  onEvent(event: CheckoutEvent): void;\n}\n\nconst listeners: CheckoutListener[] = [];\nconst seen: string[] = [];\n\nfunction subscribe(listener: CheckoutListener): void {\n  listeners.push(listener);\n}\n\nfunction emit(event: CheckoutEvent): void {\n  for (const l of listeners) l.onEvent(event);\n}\n\n// Handles every event: must keep compiling.\nsubscribe({ onEvent: (e) => { seen.push(`${e.type}:${e.orderId}`); } });\n\n// A listener that takes no arguments at all: must keep compiling too.\nsubscribe({ onEvent: () => { seen.push('ping'); } });\n\n// ---- Type tests (do not edit) ----\n// This listener only understands PaymentFailed; an OrderShipped event would\n// crash it (e.reason is undefined). Subscribing it must be a type error.\nconst failuresOnly = { onEvent: (e: PaymentFailed) => { seen.push(e.reason.toUpperCase()); } };\n// @ts-expect-error: a PaymentFailed-only listener cannot receive every CheckoutEvent\nconst unsafe: CheckoutListener = failuresOnly;\n\nemit({ type: 'order-shipped', orderId: 1042, carrier: 'DHL' });\nconsole.log(seen);\n"},"solution":{"ts":"type PaymentFailed = { type: 'payment-failed'; orderId: number; reason: string };\ntype OrderShipped = { type: 'order-shipped'; orderId: number; carrier: string };\ntype CheckoutEvent = PaymentFailed | OrderShipped;\n\n// Cartwheel's checkout event bus.\n// Property syntax: the parameter is checked strictly (contravariantly).\ninterface CheckoutListener {\n  onEvent: (event: CheckoutEvent) => void;\n}\n\nconst listeners: CheckoutListener[] = [];\nconst seen: string[] = [];\n\nfunction subscribe(listener: CheckoutListener): void {\n  listeners.push(listener);\n}\n\nfunction emit(event: CheckoutEvent): void {\n  for (const l of listeners) l.onEvent(event);\n}\n\n// Handles every event: must keep compiling.\nsubscribe({ onEvent: (e) => { seen.push(`${e.type}:${e.orderId}`); } });\n\n// A listener that takes no arguments at all: must keep compiling too.\nsubscribe({ onEvent: () => { seen.push('ping'); } });\n\n// ---- Type tests (do not edit) ----\n// This listener only understands PaymentFailed; an OrderShipped event would\n// crash it (e.reason is undefined). Subscribing it must be a type error.\nconst failuresOnly = { onEvent: (e: PaymentFailed) => { seen.push(e.reason.toUpperCase()); } };\n// @ts-expect-error: a PaymentFailed-only listener cannot receive every CheckoutEvent\nconst unsafe: CheckoutListener = failuresOnly;\n\nemit({ type: 'order-shipped', orderId: 1042, carrier: 'DHL' });\nconsole.log(seen);\n"},"checks":[{"id":"types","typecheck":true},{"id":"still-delivers","test":"return seen.includes('order-shipped:1042') && seen.includes('ping');"}],"title":"Close the listener loophole","prompt":"<p><code dir=\"ltr\">CheckoutListener</code> declares <code dir=\"ltr\">onEvent</code> with method syntax, so TypeScript checks its parameter bivariantly and happily accepts <code dir=\"ltr\">failuresOnly</code>, a listener that would crash on an <code dir=\"ltr\">OrderShipped</code> event. Change how <code dir=\"ltr\">onEvent</code> is declared (not its parameter type) so that assigning <code dir=\"ltr\">failuresOnly</code> to a <code dir=\"ltr\">CheckoutListener</code> is a type error, while the two existing <code dir=\"ltr\">subscribe</code> calls keep compiling and the shipped event still reaches them.</p>\n","hints":["The fix is one line inside the interface.","Method syntax is <code dir=\"ltr\">name(arg: T): void</code>. Property syntax is <code dir=\"ltr\">name: (arg: T) =&gt; void</code>.","Write <code dir=\"ltr\">onEvent: (event: CheckoutEvent) =&gt; void;</code>."],"explanation":"<p>With method syntax, <code dir=\"ltr\">strictFunctionTypes</code> does not apply, so a listener that accepts only <code dir=\"ltr\">PaymentFailed</code> passes as one that accepts every <code dir=\"ltr\">CheckoutEvent</code>. Declared as a property with a function type, the parameter is checked contravariantly: the listener must accept at least everything the bus can send. <code dir=\"ltr\">(e) =&gt; …</code> gets its parameter type from context and <code dir=\"ltr\">() =&gt; …</code> ignores the argument, so both keep compiling. The <code dir=\"ltr\">@ts-expect-error</code> line now has a real error to expect.</p>\n","checkLabels":{"types":"The file type-checks with zero errors: the unsafe listener is rejected","still-delivers":"Both safe listeners still receive the shipped event"}},"words":759,"readMinutes":4}