ts-04-event-emitter
1.000
Challenge · difficulty 4/5
# Typed event emitter
Implement **`solution.ts`** exporting a generic, fully typed event emitter:
```ts
export class EventEmitter<Events extends Record<string, unknown[]>> {
on<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): () => void;
off<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): void;
emit<E extends keyof Events>(event: E, ...args: Events[E]): void;
}
```
`Events` maps an event name to the **tuple of argument types** its listeners receive.
- `on(event, fn)` registers a listener and returns an **unsubscribe** function. Calling
the returned function removes that exact listener.
- `off(event, fn)` removes a previously registered listener. Removing a listener that is
not registered is a no-op.
- `emit(event, ...args)` calls every listener registered for `event`, **in the order they
were registered**, passing `args`. Emitting an event with no listeners is a no-op.
- The same function may be registered more than once; each registration is independent.
Example:
```ts
type Events = {
message: [text: string];
count: [n: number, label: string];
};
const ee = new EventEmitter<Events>();
const unsub = ee.on("message", (text) => console.log(text));
ee.emit("message", "hi"); // logs "hi"
unsub();
ee.emit("message", "bye"); // nothing logged
```
Keep it fully typed (must pass `tsc --noEmit` in strict mode). Do not use `any` in the
public API.
tests/solution.test.ts
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { EventEmitter } from "./solution.ts";
type Events = {
message: [text: string];
count: [n: number, label: string];
};
test("calls a registered listener with args", () => {
const ee = new EventEmitter<Events>();
const seen: string[] = [];
ee.on("message", (text) => seen.push(text));
ee.emit("message", "hi");
assert.deepEqual(seen, ["hi"]);
});
test("multiple listeners fire in registration order", () => {
const ee = new EventEmitter<Events>();
const order: number[] = [];
ee.on("message", () => order.push(1));
ee.on("message", () => order.push(2));
ee.on("message", () => order.push(3));
ee.emit("message", "x");
assert.deepEqual(order, [1, 2, 3]);
});
test("passes multiple typed args", () => {
const ee = new EventEmitter<Events>();
let received: [number, string] | undefined;
ee.on("count", (n, label) => {
received = [n, label];
});
ee.emit("count", 7, "items");
assert.deepEqual(received, [7, "items"]);
});
test("returned unsubscribe removes the listener", () => {
const ee = new EventEmitter<Events>();
const seen: string[] = [];
const unsub = ee.on("message", (t) => seen.push(t));
ee.emit("message", "a");
unsub();
ee.emit("message", "b");
assert.deepEqual(seen, ["a"]);
});
test("off removes a specific listener, leaving others", () => {
const ee = new EventEmitter<Events>();
const seen: string[] = [];
const a = (t: string) => seen.push("a:" + t);
const b = (t: string) => seen.push("b:" + t);
ee.on("message", a);
ee.on("message", b);
ee.off("message", a);
ee.emit("message", "x");
assert.deepEqual(seen, ["b:x"]);
});
test("emitting an event with no listeners is a no-op", () => {
const ee = new EventEmitter<Events>();
assert.doesNotThrow(() => ee.emit("message", "nobody"));
});
test("off on an unregistered listener is a no-op", () => {
const ee = new EventEmitter<Events>();
assert.doesNotThrow(() => ee.off("message", () => {}));
});
test("the same function registered twice fires twice", () => {
const ee = new EventEmitter<Events>();
let calls = 0;
const fn = () => {
calls += 1;
};
ee.on("message", fn);
ee.on("message", fn);
ee.emit("message", "x");
assert.equal(calls, 2);
});
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```typescript
export class EventEmitter<Events extends Record<string, unknown[]>> {
private listeners: Map<keyof Events, Array<(...args: unknown[]) => void>> = new Map();
on<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): () => void {
const handlers = this.listeners.get(event) || [];
handlers.push(fn);
this.listeners.set(event, handlers);
return () => {
const currentHandlers = this.listeners.get(event);
if (currentHandlers) {
const index = currentHandlers.indexOf(fn);
if (index !== -1) {
currentHandlers.splice(index, 1);
if (currentHandlers.length === 0) {
this.listeners.delete(event);
}
}
}
};
}
off<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): void {
const handlers = this.listeners.get(event);
if (handlers) {
const index = handlers.indexOf(fn);
if (index !== -1) {
handlers.splice(index, 1);
if (handlers.length === 0) {
this.listeners.delete(event);
}
}
}
}
emit<E extends keyof Events>(event: E, ...args: Events[E]): void {
const handlers = this.listeners.get(event);
if (handlers) {
// Create a copy to avoid issues if listeners are added/removed during emission
[...handlers].forEach(handler => handler(...args));
}
}
}
```Test output (stdout)
TAP version 13 # Subtest: calls a registered listener with args ok 1 - calls a registered listener with args --- duration_ms: 0.625372 type: 'test' ... # Subtest: multiple listeners fire in registration order ok 2 - multiple listeners fire in registration order --- duration_ms: 0.086251 type: 'test' ... # Subtest: passes multiple typed args ok 3 - passes multiple typed args --- duration_ms: 0.05287 type: 'test' ... # Subtest: returned unsubscribe removes the listener ok 4 - returned unsubscribe removes the listener --- duration_ms: 0.05633 type: 'test' ... # Subtest: off removes a specific listener, leaving others ok 5 - off removes a specific listener, leaving others --- duration_ms: 0.072501 type: 'test' ... # Subtest: emitting an event with no listeners is a no-op ok 6 - emitting an event with no listeners is a no-op --- duration_ms: 0.07757 type: 'test' ... # Subtest: off on an unregistered listener is a no-op ok 7 - off on an unregistered listener is a no-op --- duration_ms: 0.03961 type: 'test' ... # Subtest: the same function registered twice fires twice ok 8 - the same function registered twice fires twice --- duration_ms: 0.056891 type: 'test' ... 1..8 # tests 8 # suites 0 # pass 8 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 74.728834