1

I am trying to implement an extensible EventEmitter pattern in TypeScript.

This is the way it is supposed to be used:

class DataEmitter<T, _EE extends Dict<any[]>> extends EventEmitter<DefaultMerge<_EE, { data: [T] }>> {
 // ...
}

So any class extending DataEmitter might pass another object type defining new events it is capable of emitting, and so on.

This is how I tried to define it:

type Listener<T extends any[]> = (...args: T) => void

type Dict<V> = {
  [name: string]: V
}

type DefaultMerge<A, B> = A & B

type EventEmitter_EE<_EE extends Dict<any[]> = {}> = DefaultMerge<
  _EE,
  {
    listen: [{ event: 'listen' | keyof _EE }]
  }
>

interface EE<_EE extends Dict<any[]> = {}> {
  addListener<K extends keyof _EE>(
    event: K,
    listener: Listener<_EE[K]>
  ): void

  emit<K extends keyof _EE>(event: K, ...args: _EE[K]): void
}

class EventEmitter<_EE extends Dict<any[]> = {}>
  implements EE<EventEmitter_EE<_EE>>
{
  private __listeners: {
    [K in keyof EventEmitter_EE<_EE>]?: Listener<EventEmitter_EE<_EE>[K]>[]
  } = {}

  addListener<K extends keyof EventEmitter_EE<_EE>>(
    event: K,
    listener: Listener<EventEmitter_EE<_EE>[K]>
  ): void {
    this.__listeners[event] = this.__listeners[event] || []
    this.__listeners[event].push(listener)

    // >> TypeScript Error << \\
    this.emit('listen', { event })
  }

  emit<K extends keyof EventEmitter_EE<_EE>>(
    event: K,
    ...args: EventEmitter_EE<_EE>[K]
  ): void {
    const listeners = [...(this.__listeners[event] || [])]

    for (const listener of listeners) {
      listener.call(this, ...args)
    }
  }
}

Note the EventEmitter class comes with a default event called "listen".

However, TypeScript complains about the referred line with the following error message:

Argument of type '[{ event: K; }]' is not assignable to parameter of type
 '_EE["listen"] & [{ event: "listen" | keyof _EE; }]'.
  Type '[{ event: K; }]' is not assignable to type '_EE["listen"]'.

Ideally, it should understand that "listen" is a default event of EventEmitter and infer its type to be "listen" | keyof _EE as defined.

My guess is that DefaultMerge<A, B> is not well defined; it should return an object type that will "first look in B, then, if not defined, look at A".

How do I get rid of this error?

Samuel
  • 1,271
  • 1
  • 15
  • 29

1 Answers1

1
// >> TypeScript Error << \\
this.emit('listen', { event })

The error in the above case is because TypeScript doesn't know what _EE is within the class, and considering EventEmitter_EE has been defined as an intersection, it tries to infer the type as:

'_EE["listen"] & [{ event: "listen" | keyof _EE; }]'

The same statement works in below cases, where TypeScript is able to infer _EE:

class SomeClass {
  someMethod() {
    const eventEmitter = new EventEmitter();
    const event = 'listen';
    eventEmitter.addListener(event, () => console.log('eventOccured'));
    eventEmitter.emit('listen', { event });
  }
}
type click_EE = {
  click: [{event: 'click'}]
}
class SomeClass {
  someMethod() {
    const eventEmitter = new EventEmitter<click_EE>();
    const event = 'listen';
    eventEmitter.addListener(event, () => console.log('eventOccured'));
    eventEmitter.emit('listen', { event });
  }
}

You can also check Extensible, strongly typed Event Emitter Interface in Typescript and see if it helps.

Siddhant
  • 2,911
  • 2
  • 8
  • 14