{"id":"l-at-4-4","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Build an event emitter whose event names and payloads are checked from one event map, and a builder whose result type grows with each method call.","html":"<p>Cartwheel’s checkout emits events: <code dir=\"ltr\">order.shipped</code>, <code dir=\"ltr\">payment.failed</code>, <code dir=\"ltr\">checkout.opened</code>. Analytics, the email service and the order page all listen. The first version used a plain emitter typed with <code dir=\"ltr\">string</code> names and <code dir=\"ltr\">any</code> payloads, and it produced the two classic bugs: a listener for <code dir=\"ltr\">'order.shiped'</code> that never fired, and a handler reading <code dir=\"ltr\">payload.trackingNo</code> on an event that sends <code dir=\"ltr\">trackingNumber</code>. Both were invisible until someone noticed missing emails.</p>\n<p>Everything from this section and the last comes together to fix it.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">One map, every type</h2>\n<p>Describe every event once, in a map from name to payload type:</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\"> CheckoutEvents</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:#0A3069;--shiki-dark:#A5D6FF\">  'checkout.opened'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'order.shipped'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">orderId</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\">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:#953800;--shiki-dark:#FFA657\">trackingNumber</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  'payment.failed'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">orderId</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\">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:#1F2328;--shiki-dark:#E6EDF3\">};</span></span></code></pre></div>\n<p>You could also generate part of this map with the template-literal mapped type from <a href=\"/lesson/advanced-typescript/l-at-3-4\">Template Literal Types</a>. Either way, the map is the single source of truth: names are its keys, payloads are its values.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">The typed emitter</h2>\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\"> EventArgs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">P</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\">P</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">extends</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">undefined</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:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#953800;--shiki-dark:#FFA657\">payload</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#953800;--shiki-dark:#FFA657\">P</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\"> Handler</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">payload</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 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\">class</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Emitter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Events</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</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:#0550AE;--shiki-dark:#79C0FF\">unknown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  #handlers</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Events</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">Set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Handler</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\">  on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Events</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">handler</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">payload</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Events</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</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:#CF222E;--shiki-dark:#FF7B72\">:</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\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> set </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> this</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.#handlers.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event);</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\">set) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      set </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      this</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.#handlers.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event, set);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    set.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(handler);</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:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> set.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">delete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(handler);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  emit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Events</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">args</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> EventArgs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Events</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</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\">    for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> handler</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> this</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.#handlers.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event) </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:#6E7781;--shiki-dark:#8B949E\">      // Handlers for K were registered with a matching payload type in on().</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      (handler </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">payload</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\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)(args[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The pattern is the API client’s, applied to events. <code dir=\"ltr\">K</code> is inferred from the event name and has one job: look up the payload in <code dir=\"ltr\">Events[K]</code>. In <code dir=\"ltr\">on</code>, that type flows into the handler’s parameter, so the callback is contextually typed. In <code dir=\"ltr\">emit</code>, it checks the payload you pass.</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\"> bus</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Emitter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">CheckoutEvents</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\"> off</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> bus.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</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\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sendEmail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Tracking: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">trackingNumber</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">bus.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">emit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'order.shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { orderId: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, carrier: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'DHL'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, trackingNumber: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'1Z-999'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">bus.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">emit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'checkout.opened'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);                         </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// no payload needed</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">bus.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'order.shiped'</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:#6E7781;--shiki-dark:#8B949E\">// Error: Argument of type '\"order.shiped\"' is not assignable to parameter of type 'keyof CheckoutEvents'.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">bus.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">emit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'payment.failed'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { orderId: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1042</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: … Property 'reason' is missing …</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">off</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// unsubscribe</span></span></code></pre></div>\n<p><code dir=\"ltr\">EventArgs</code> is the conditional rest-parameter trick from <a href=\"/lesson/advanced-typescript/l-at-3-3\">Conditional Types, Distribution and infer</a>: payload-less events take zero extra arguments, the rest take exactly one. The brackets in <code dir=\"ltr\">[P] extends [undefined]</code> stop distribution, so a payload type like <code dir=\"ltr\">string | undefined</code> is still required rather than split.</p>\n<p>Notice where the loose types live. Inside the class, handlers are stored as <code dir=\"ltr\">Set&lt;Handler&gt;</code> and called through one commented cast, because a <code dir=\"ltr\">Map</code> cannot express “the value type depends on the key”. Outside, every method is precise. That split, untyped storage behind a typed surface, is how most well-typed libraries are built. The private <code dir=\"ltr\">#handlers</code> field is a real JavaScript private field, so nothing outside can reach in and break the invariant.</p>\n<p>Once the core is typed, new methods inherit the precision almost for free. A <code dir=\"ltr\">once</code> that resolves a promise with the next payload is four lines, and its result is typed per event with no extra annotations:</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:#1F2328;--shiki-dark:#E6EDF3\">once&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Events</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Events</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</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:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">resolve</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:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> off</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> this</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">payload</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:#8250DF;--shiki-dark:#D2A8FF\">off</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(); </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">resolve</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(payload); });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\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\"> shipped</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> bus.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">once</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'order.shipped'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// { orderId; carrier; trackingNumber }</span></span></code></pre></div>\n<p>This is the payoff of putting the types in one map and threading <code dir=\"ltr\">K</code> through: each method you add is checked against the same source of truth, and none of them repeats a payload type.</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>Typing the event parameter as keyof Events directly</span></p>\n<p><code dir=\"ltr\">on(event: keyof Events, handler: (payload: Events[keyof Events]) =&gt; void)</code> looks almost the same and compiles. But without a type parameter, the handler’s payload is the union of <em>every</em> payload, so <code dir=\"ltr\">e.trackingNumber</code> is an error on <code dir=\"ltr\">order.shipped</code> and <code dir=\"ltr\">e.reason</code> would be “allowed” only after narrowing a value that has no discriminant. The type parameter is what links the specific name to its specific payload.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Builders whose type grows</h2>\n<p>A builder chains method calls to assemble something step by step. The type-level trick is to carry what has been built so far in a type parameter, and return a new, wider type from each call. Here is a small query builder for Cartwheel’s order history:</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\">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 style=\"color:#953800;--shiki-dark:#FFA657\">totalCents</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\">customer</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\">class</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderQuery</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Selected</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> never</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  readonly</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> #fields</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> readonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</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\">  constructor</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">fields</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> readonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> []) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    this</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.#fields </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> fields;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">fields</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[])</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> OrderQuery</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Selected</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</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:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> OrderQuery</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Selected</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>([</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">this</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.#fields, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">fields]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">rows</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[])</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Pick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</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\">Selected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>[] {</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">    // #fields holds exactly the keys in Selected.</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rows.</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\">row</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fromEntries</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">this</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.#fields.</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\">f</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [f, row[f]])) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Pick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</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\">Selected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\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\"> rows</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> OrderQuery</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'id'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'totalCents'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(allOrders);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">rows[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].totalCents; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// number</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">rows[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].customer;   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Property 'customer' does not exist on type 'Pick&#x3C;Order, \"id\" | \"totalCents\">'.</span></span></code></pre></div>\n<p><code dir=\"ltr\">Selected</code> starts as <code dir=\"ltr\">never</code> (nothing selected) and each <code dir=\"ltr\">select</code> returns <code dir=\"ltr\">OrderQuery&lt;Selected | K&gt;</code>. A method cannot change the type of the object it is called on, so each step returns a new instance. That is also good runtime design: builders that never mutate can be shared and reused safely. This is the same technique query builders such as Kysely and Drizzle use at much larger scale.</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>Keep builder types shallow</span></p>\n<p>Every step of a builder adds to a type the editor must display and the compiler must check. A builder with ten type parameters and conditional types on every method produces error messages nobody can read. Track the one or two facts callers actually need, such as which fields are selected, and keep everything else simple.</p>\n</aside>\n<p>In the exercise you will build Cartwheel’s typed event bus and a small selecting builder. That completes the patterns section; section 5 turns to shipping all of this, starting with the compiler flags that make these types trustworthy.</p>\n","toc":[{"id":"sec-1","text":"One map, every type","level":2},{"id":"sec-2","text":"The typed emitter","level":2},{"id":"sec-3","text":"Builders whose type grows","level":2}],"takeaways":["An event map type (<code dir=\"ltr\">{ 'order.shipped': { orderId: number } }</code>) is the single source of truth for both event names and payload types.","<code dir=\"ltr\">on&lt;K extends keyof Events&gt;(event: K, handler: (payload: Events[K]) =&gt; void)</code> ties the handler’s parameter to the event name the caller passes.","A conditional rest parameter lets events with no payload be emitted without a dummy argument.","A builder can carry state in a type parameter, returning a new, wider type from each call, so the final result type reflects every step.","Keep the untyped storage inside the class and the precise types on its public methods, so the one cast lives in one place."],"further":[{"title":"Generic Classes","url":"https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-classes"},{"title":"Classes (this-based types and parameter properties)","url":"https://www.typescriptlang.org/docs/handbook/2/classes.html"},{"title":"Rest parameters with tuple types (TypeScript 3.0 release notes)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html"}],"quiz":[{"q":"<p>With <code dir=\"ltr\">bus = new Emitter&lt;CheckoutEvents&gt;()</code>, what is the type of <code dir=\"ltr\">p</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\"> CheckoutEvents</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:#0A3069;--shiki-dark:#A5D6FF\">  'order.shipped'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">orderId</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\">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:#0A3069;--shiki-dark:#A5D6FF\">  'payment.failed'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">orderId</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\">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:#1F2328;--shiki-dark:#E6EDF3\">};</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">bus.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</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\">p</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\">(p.reason));</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">CheckoutEvents[keyof CheckoutEvents]</code>, the union of both payloads","why":"That would happen if the event name were typed as <code dir=\"ltr\">keyof Events</code> without a type parameter. <code dir=\"ltr\">K</code> captures the specific name."},{"text":"<code dir=\"ltr\">unknown</code>, because handlers are stored in an untyped map","why":"How handlers are stored is an implementation detail; the public signature decides what callers see."},{"text":"<code dir=\"ltr\">any</code>, because callbacks are contextually typed","why":"Contextual typing gives <code dir=\"ltr\">p</code> the parameter type from the signature, which is precise here."},{"text":"<code dir=\"ltr\">{ orderId: number; reason: string }</code>","why":"Correct. <code dir=\"ltr\">K</code> is inferred as <code dir=\"ltr\">'payment.failed'</code>, and the handler’s parameter is <code dir=\"ltr\">CheckoutEvents[K]</code>."}],"answer":3},{"q":"<p>Why type <code dir=\"ltr\">emit</code> with <code dir=\"ltr\">...args: EventArgs&lt;Events[K]&gt;</code> instead of <code dir=\"ltr\">payload: Events[K]</code>?</p>","options":[{"text":"So events whose payload is <code dir=\"ltr\">undefined</code> can be emitted as <code dir=\"ltr\">emit('checkout.opened')</code> with no second argument.","why":"Correct. The conditional produces an empty tuple for payload-less events and a one-element tuple otherwise."},{"text":"Because rest parameters are faster at runtime.","why":"The choice is about the type signature; at runtime the difference is negligible."},{"text":"Because <code dir=\"ltr\">Events[K]</code> cannot be used as a parameter type.","why":"It can; <code dir=\"ltr\">on</code> uses it for the handler parameter. The rest form only solves the missing-payload case."}],"answer":0},{"q":"<p>What is the type of <code dir=\"ltr\">rows</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> rows</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> OrderQuery</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'id'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'totalCents'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(allOrders);</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">Order[]</code>, because <code dir=\"ltr\">run</code> always returns full orders","why":"In this builder <code dir=\"ltr\">run</code> returns <code dir=\"ltr\">Pick&lt;Order, Selected&gt;[]</code>, and <code dir=\"ltr\">Selected</code> has grown with each call."},{"text":"<code dir=\"ltr\">Pick&lt;Order, 'id' | 'totalCents'&gt;[]</code>","why":"Correct. Each <code dir=\"ltr\">select</code> returns <code dir=\"ltr\">OrderQuery&lt;Selected | K&gt;</code>, so the type parameter accumulates both fields."},{"text":"<code dir=\"ltr\">Pick&lt;Order, 'totalCents'&gt;[]</code>, because the second call replaces the first","why":"The second call widens <code dir=\"ltr\">Selected</code> with a union; it does not replace it."}],"answer":1},{"q":"<p>Why does each <code dir=\"ltr\">select</code> call return a new <code dir=\"ltr\">OrderQuery</code> instead of mutating <code dir=\"ltr\">this</code> and returning it?</p>","options":[{"text":"Because classes cannot return <code dir=\"ltr\">this</code> from methods.","why":"They can; fluent APIs often do. The issue is what the type of <code dir=\"ltr\">this</code> can express."},{"text":"Because a method cannot change the type of the object it is called on, so the wider type needs a new value.","why":"Correct. <code dir=\"ltr\">this</code> keeps its type for its whole life; returning a new instance with a new type parameter is how the type grows."},{"text":"Because mutation is not allowed in TypeScript classes.","why":"TypeScript allows mutation freely; this is a typing constraint, not a language rule."}],"answer":1}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype Order = { id: number; status: OrderStatus; totalCents: number; customer: string };\n\nconst ORDERS: Order[] = [\n  { id: 1042, status: 'shipped', totalCents: 18900, customer: 'Mona' },\n  { id: 1043, status: 'processing', totalCents: 4500, customer: 'Karim' },\n];\n\nclass Emitter<Events> {\n  #handlers = new Map<string, Set<(payload: any) => void>>();\n\n  on(event: string, handler: (payload: any) => void): () => void {\n    let set = this.#handlers.get(event);\n    if (!set) {\n      set = new Set();\n      this.#handlers.set(event, set);\n    }\n    set.add(handler);\n    return () => set.delete(handler);\n  }\n\n  emit(event: string, payload?: any): void {\n    for (const handler of this.#handlers.get(event) ?? []) handler(payload);\n  }\n}\n\nclass OrderQuery {\n  readonly #fields: readonly (keyof Order)[];\n\n  constructor(fields: readonly (keyof Order)[] = []) {\n    this.#fields = fields;\n  }\n\n  select(...fields: (keyof Order)[]): OrderQuery {\n    return new OrderQuery([...this.#fields, ...fields]);\n  }\n\n  run(rows: Order[]): Partial<Order>[] {\n    return rows.map((row) => Object.fromEntries(this.#fields.map((f) => [f, row[f]])));\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype CheckoutEvents = {\n  'checkout.opened': undefined;\n  'order.shipped': { orderId: number; carrier: string };\n  'payment.failed': { orderId: number; reason: string };\n};\n\nfunction typeTests(bus: Emitter<CheckoutEvents>) {\n  bus.on('payment.failed', (e) => {\n    type T1 = Expect<Equal<typeof e, { orderId: number; reason: string }>>;\n  });\n  bus.emit('checkout.opened');\n  bus.emit('order.shipped', { orderId: 1042, carrier: 'DHL' });\n  // @ts-expect-error: 'order.shiped' is a typo\n  bus.on('order.shiped', () => {});\n  // @ts-expect-error: payment.failed needs a reason\n  bus.emit('payment.failed', { orderId: 1042 });\n  // @ts-expect-error: order.shipped needs a payload\n  bus.emit('order.shipped');\n}\n\nconst rows = new OrderQuery().select('id', 'status').run(ORDERS);\ntype T2 = Expect<Equal<typeof rows, Pick<Order, 'id' | 'status'>[]>>;\nconst more = new OrderQuery().select('id').select('totalCents').run(ORDERS);\ntype T3 = Expect<Equal<typeof more, Pick<Order, 'id' | 'totalCents'>[]>>;\n\nconsole.log(rows, more);\n"},"solution":{"ts":"type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';\ntype Order = { id: number; status: OrderStatus; totalCents: number; customer: string };\n\nconst ORDERS: Order[] = [\n  { id: 1042, status: 'shipped', totalCents: 18900, customer: 'Mona' },\n  { id: 1043, status: 'processing', totalCents: 4500, customer: 'Karim' },\n];\n\ntype EventArgs<P> = [P] extends [undefined] ? [] : [payload: P];\ntype Handler = (payload: never) => void;\n\nclass Emitter<Events extends Record<string, unknown>> {\n  #handlers = new Map<keyof Events, Set<Handler>>();\n\n  on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): () => void {\n    let set = this.#handlers.get(event);\n    if (!set) {\n      set = new Set();\n      this.#handlers.set(event, set);\n    }\n    set.add(handler);\n    return () => set.delete(handler);\n  }\n\n  emit<K extends keyof Events>(event: K, ...args: EventArgs<Events[K]>): void {\n    for (const handler of this.#handlers.get(event) ?? []) {\n      // Handlers for K were registered with a matching payload type in on().\n      (handler as (payload: unknown) => void)(args[0]);\n    }\n  }\n}\n\nclass OrderQuery<Selected extends keyof Order = never> {\n  readonly #fields: readonly (keyof Order)[];\n\n  constructor(fields: readonly (keyof Order)[] = []) {\n    this.#fields = fields;\n  }\n\n  select<K extends keyof Order>(...fields: K[]): OrderQuery<Selected | K> {\n    return new OrderQuery<Selected | K>([...this.#fields, ...fields]);\n  }\n\n  run(rows: Order[]): Pick<Order, Selected>[] {\n    // #fields holds exactly the keys in Selected.\n    return rows.map((row) => Object.fromEntries(this.#fields.map((f) => [f, row[f]])) as Pick<Order, Selected>);\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype CheckoutEvents = {\n  'checkout.opened': undefined;\n  'order.shipped': { orderId: number; carrier: string };\n  'payment.failed': { orderId: number; reason: string };\n};\n\nfunction typeTests(bus: Emitter<CheckoutEvents>) {\n  bus.on('payment.failed', (e) => {\n    type T1 = Expect<Equal<typeof e, { orderId: number; reason: string }>>;\n  });\n  bus.emit('checkout.opened');\n  bus.emit('order.shipped', { orderId: 1042, carrier: 'DHL' });\n  // @ts-expect-error: 'order.shiped' is a typo\n  bus.on('order.shiped', () => {});\n  // @ts-expect-error: payment.failed needs a reason\n  bus.emit('payment.failed', { orderId: 1042 });\n  // @ts-expect-error: order.shipped needs a payload\n  bus.emit('order.shipped');\n}\n\nconst rows = new OrderQuery().select('id', 'status').run(ORDERS);\ntype T2 = Expect<Equal<typeof rows, Pick<Order, 'id' | 'status'>[]>>;\nconst more = new OrderQuery().select('id').select('totalCents').run(ORDERS);\ntype T3 = Expect<Equal<typeof more, Pick<Order, 'id' | 'totalCents'>[]>>;\n\nconsole.log(rows, more);\n"},"checks":[{"id":"types","typecheck":true},{"id":"delivers","test":"const bus = new Emitter(); const got = []; bus.on('a', (p) => got.push('a:' + p.n)); bus.on('b', (p) => got.push('b')); bus.emit('a', { n: 1 }); return got.join() === 'a:1';"},{"id":"unsubscribes","test":"const bus = new Emitter(); let n = 0; const off = bus.on('x', () => { n++; }); bus.emit('x'); off(); bus.emit('x'); return n === 1;"},{"id":"selects","test":"const r = new OrderQuery().select('id').select('customer').run([{ id: 1, status: 'shipped', totalCents: 5, customer: 'Lea' }]); return JSON.stringify(r) === '[{\"id\":1,\"customer\":\"Lea\"}]';"}],"title":"Type the event bus and the query builder","prompt":"<p>Make Cartwheel’s two utilities fully typed without changing their runtime behaviour:</p>\n<ol>\n<li><code dir=\"ltr\">Emitter&lt;Events&gt;</code>: <code dir=\"ltr\">on(event, handler)</code> must only accept event names from <code dir=\"ltr\">Events</code> and give the handler that event’s payload type; <code dir=\"ltr\">emit(event, payload)</code> must check the payload, and events whose payload is <code dir=\"ltr\">undefined</code> must be emitted with no payload argument.</li>\n<li><code dir=\"ltr\">OrderQuery</code>: each <code dir=\"ltr\">select(...fields)</code> call adds to the selected fields, and <code dir=\"ltr\">run(rows)</code> returns <code dir=\"ltr\">Pick&lt;Order, …&gt;[]</code> of exactly the selected fields.</li>\n</ol>\n<p>Keep any cast inside the classes, in one commented place each.</p>\n","hints":["Give <code dir=\"ltr\">on</code> and <code dir=\"ltr\">emit</code> a type parameter <code dir=\"ltr\">K extends keyof Events</code> and look the payload up with <code dir=\"ltr\">Events[K]</code>. Constrain the class: <code dir=\"ltr\">Emitter&lt;Events extends Record&lt;string, unknown&gt;&gt;</code>.","For payload-less events, use a conditional rest parameter: <code dir=\"ltr\">type EventArgs&lt;P&gt; = [P] extends [undefined] ? [] : [payload: P];</code> and <code dir=\"ltr\">emit&lt;K …&gt;(event: K, ...args: EventArgs&lt;Events[K]&gt;)</code>.","Give <code dir=\"ltr\">OrderQuery</code> a type parameter <code dir=\"ltr\">Selected extends keyof Order = never</code>; <code dir=\"ltr\">select&lt;K extends keyof Order&gt;(...fields: K[])</code> returns <code dir=\"ltr\">new OrderQuery&lt;Selected | K&gt;(…)</code>, and <code dir=\"ltr\">run</code> returns <code dir=\"ltr\">Pick&lt;Order, Selected&gt;[]</code> with one cast."],"explanation":"<p>The event map is the only place payload types are written; <code dir=\"ltr\">K</code> is inferred from the event name and links it to <code dir=\"ltr\">Events[K]</code> for both the handler and the payload. The conditional rest parameter yields zero arguments for <code dir=\"ltr\">undefined</code> payloads, and the brackets stop it from distributing over unions. Inside, a <code dir=\"ltr\">Map</code> cannot relate keys to value types, so handlers are stored loosely and called through one cast. The builder carries the selected keys in its type parameter: each <code dir=\"ltr\">select</code> returns a new instance with a wider union, and <code dir=\"ltr\">run</code> turns that union into a <code dir=\"ltr\">Pick</code>.</p>\n","checkLabels":{"types":"The file type-checks with zero errors, including all type tests","delivers":"<code dir=\"ltr\">emit</code> calls only the handlers for that event, with the payload","unsubscribes":"The function returned by <code dir=\"ltr\">on</code> removes the handler","selects":"<code dir=\"ltr\">run</code> returns only the selected fields, across several <code dir=\"ltr\">select</code> calls"}},"words":665,"readMinutes":3}