← run

ts-04-event-emitter

1.000
8/8 tests· typing
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
```ts
export class EventEmitter<Events extends Record<string, unknown[]>> {
  private listeners = new Map<keyof Events, Array<(...args: unknown[]) => void>>();

  on<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): () => void {
    const list = this.listeners.get(event) || [];
    list.push(fn as (...args: unknown[]) => void);
    this.listeners.set(event, list);
    return () => this.off(event, fn);
  }

  off<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): void {
    const list = this.listeners.get(event);
    if (list) {
      const idx = list.indexOf(fn as (...args: unknown[]) => void);
      if (idx !== -1) {
        list.splice(idx, 1);
      }
    }
  }

  emit<E extends keyof Events>(event: E, ...args: Events[E]): void {
    const list = this.listeners.get(event);
    if (list) {
      for (const fn of list) {
        fn(...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.626921
  type: 'test'
  ...
# Subtest: multiple listeners fire in registration order
ok 2 - multiple listeners fire in registration order
  ---
  duration_ms: 0.081918
  type: 'test'
  ...
# Subtest: passes multiple typed args
ok 3 - passes multiple typed args
  ---
  duration_ms: 0.052509
  type: 'test'
  ...
# Subtest: returned unsubscribe removes the listener
ok 4 - returned unsubscribe removes the listener
  ---
  duration_ms: 0.060279
  type: 'test'
  ...
# Subtest: off removes a specific listener, leaving others
ok 5 - off removes a specific listener, leaving others
  ---
  duration_ms: 0.065609
  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.078778
  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.04043
  type: 'test'
  ...
# Subtest: the same function registered twice fires twice
ok 8 - the same function registered twice fires twice
  ---
  duration_ms: 0.057159
  type: 'test'
  ...
1..8
# tests 8
# suites 0
# pass 8
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 78.195705
Reasoning (chain-of-thought)
The user wants a fully typed `EventEmitter` class in TypeScript.
The class should be generic over `Events extends Record<string, unknown[]>`.
It needs `on`, `off`, and `emit` methods with specific signatures.
`on` should return an unsubscribe function.
`off` should remove a listener.
`emit` should call listeners in order.
Same function can be registered multiple times.
No `any` in public API.
Must pass `tsc --noEmit` in strict mode.

Let's design the internal structure.
We need to store listeners for each event.
Since `Events` is a record of event names to argument tuples, we can use a `Map` or a plain object to store arrays of listeners.
To handle the unsubscribe function, we can either store a unique ID per registration, or just store the listeners in an array and remove by reference when unsubscribing. But removing by reference from an array while iterating or keeping order might be tricky if we just splice. Actually, splicing is fine, or we can mark as removed. Or we can just use a `Set` or array and filter.
Wait, the requirement says: "Calling the returned function removes that exact listener."
If we store listeners in an array, we can find the index and splice it out. That preserves order for remaining listeners.
Alternatively, we can assign a unique ID to each registration and store listeners in a map keyed by ID, but order matters. So an array is better.

Let's define the internal type for listeners:
`type Listener<E extends keyof Events> = (...args: Events[E]) => void;`
But since `Events` is generic, we can just use `Function` or `(...args: unknown[]) => void` internally, or keep it typed. Actually, TypeScript allows storing typed functions in a less typed structure, but we want to avoid `any`. We can use `Map<keyof Events, Array<(...args: unknown[]) => void>>` but that loses type safety internally. It's fine internally as long as public API is typed. Or we can use a `Record<keyof Events, Array<(...args: unknown[]) => void>>`.

Wait, `Events` keys are strings. `keyof Events` is fine.
Let's use a `Map<string, Array<(...args: unknown[]) => void>>` internally. Or just `Record<string, Array<(...args: unknown[]) => void>>`.
Actually, to be strictly typed without `any`, we can use:
`private listeners = new Map<keyof Events, Array<(...args: unknown[]) => void>>();`
But `keyof Events` might not be assignable to `string` in some TS versions, but `Events extends Record<string, unknown[]>` ensures keys are strings.

Let's stick to a simple internal structure:
```ts
private listeners = new Map<keyof Events, Array<(...args: unknown[]) => void>>();
```
Wait, `(...args: unknown[]) => void` is fine.

For `on`:
```ts
on<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): () => void {
  const list = this.listeners.get(event) || [];
  list.push(fn as (...args: unknown[]) => void);
  this.listeners.set(event, list);
  return () => this.off(event, fn);
}
```
But `off` needs to remove the exact listener. If the same function is registered twice, `off` should remove one instance. The requirement says: "The same function may be registered more than once; each registration is independent." So `off` should remove the first matching instance, or we can track by index/ID. Using `indexOf` and `splice` removes the first match, which is standard.

Let's refine `off`:
```ts
off<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): void {
  const list = this.listeners.get(event);
  if (list) {
    const idx = list.indexOf(fn as (...args: unknown[]) => void);
    if (idx !== -1) {
      list.splice(idx, 1);
    }
  }
}
```

For `emit`:
```ts
emit<E extends keyof Events>(event: E, ...args: Events[E]): void {
  const list = this.listeners.get(event);
  if (list) {
    for (const fn of list) {
      fn(...args);
    }
  }
}
```

Wait, `fn(...args)` might complain about types because `fn` is `(...args: unknown[]) => void` and `args` is `Events[E]`. Since `Events[E]` extends `unknown[]`, it should be assignable. But to be safe, we can cast or just rely on TS inference. Actually, `fn` expects `unknown[]`, and `args` is `Events[E]` which is `unknown[]`, so it's fine.

Let's check strict mode compatibility.
`Events extends Record<string, unknown[]>`
`keyof Events` will be `string & keyof Events`.
Using `Map<keyof Events, ...>` is fine.

One minor thing: `list.indexOf(fn as (...args: unknown[]) => void)` might be problematic if TS complains about casting. We can just store `Function` or `(...args: unknown[]) => void`. Actually, we can store them as `(...args: unknown[]) => void` directly.
Alternatively, we can avoid casting by using a generic internal type, but it's simpler to just use `(...args: unknown[]) => void`.

Wait, `fn as (...args: unknown[]) => void` is fine. Or we can just use `Function` type internally, but the prompt says "Do not use `any` in the public API." Internal `any` or `Function` is probably fine, but let's stick to `(...args: unknown[]) => void`.

Actually, we can make the internal storage fully typed without casts by using a `Map<keyof Events, Array<(...args: unknown[]) => void>>`.
When pushing: `list.push(fn as (...args: unknown[]) => void)`
When iterating: `fn(...args)` works because `args` is `Events[E]` which extends `unknown[]`.

Let's test mentally:
```ts
export class EventEmitter<Events extends Record<string, unknown[]>> {
  private listeners = new Map<keyof Events, Array<(...args: unknown[]) => void>>();

  on<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): () => void {
    const list = this.listeners.get(event) || [];
    list.push(fn as (...args: unknown[]) => void);
    this.listeners.set(event, list);
    return () => this.off(event, fn);
  }

  off<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): void {
    const list = this.listeners.get(event);
    if (list) {
      const idx = list.indexOf(fn as (...args: unknown[]) => void);
      if (idx !== -1) {
        list.splice(idx, 1);
      }
    }
  }

  emit<E extends keyof Events>(event: E, ...args: Events[E]): void {
    const list = this.listeners.get(event);
    if (list) {
      for (const fn of list) 
…[19627 chars elided]…
ist = this.listeners.get(event);
    if (list) {
      for (const fn of list) {
        fn(...args);
      }
    }
  }
}
```
Looks good. I will output this. 
Note: I'll make sure formatting matches exactly. No extra text.
Done. 
Output Generation. 
(Self-Correction/Refinement during generation prep)
Wait, `fn as (...args: unknown[]) => void` might cause a warning in some strict setups if `strictFunctionTypes` is on, but it's a cast so it's fine. Alternatively, I can just store as `Function` to be safe, but I'll stick with this. It's standard.
Actually, to be perfectly clean without casts, I could use:
`private listeners = new Map<keyof Events, Array<(...args: unknown[]) => void>>();`
And push `fn` directly if I change the array type to `Array<(...args: unknown[]) => void>`. TS will complain about pushing a more specific type. So cast is necessary.
I'll keep it.
All good.
Output matches. 
Proceeds. 
[Final Check]
The prompt asks for `solution.ts` exporting the class.
I will wrap it in a code block.
Done. 
Output Generation. 
```typescript
export class EventEmitter<Events extends Record<string, unknown[]>> {
  private listeners = new Map<keyof Events, Array<(...args: unknown[]) => void>>();

  on<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): () => void {
    const list = this.listeners.get(event) || [];
    list.push(fn as (...args: unknown[]) => void);
    this.listeners.set(event, list);
    return () => this.off(event, fn);
  }

  off<E extends keyof Events>(event: E, fn: (...args: Events[E]) => void): void {
    const list = this.listeners.get(event);
    if (list) {
      const idx = list.indexOf(fn as (...args: unknown[]) => void);
      if (idx !== -1) {
        list.splice(idx, 1);
      }
    }
  }

  emit<E extends keyof Events>(event: E, ...args: Events[E]): void {
    const list = this.listeners.get(event);
    if (list) {
      for (const fn of list) {
        fn(...args);
      }
    }
  }
}
```
Matches perfectly. Output matches request.
Done. 
[Output Generation] -> *Proceeds*