Is it possible to strongly type the return value of this function?

366 views Asked by At

I have a generic helper function that allows me to await events on any EventEmitter.

import { type EventEmitter } from 'eventemitter3'

/** Returns a promise that resolves when the given event fires */
export const eventPromise = async (emitter: EventEmitter, event: string) =>
  new Promise<any>(resolve => {
    emitter.once(event, d => resolve(d))
  })

So I can await the next foo event as follows:

await eventPromise(someEmitter, 'foo')

So far this works fine.

I'm using eventemitter3, which gives me strongly typed events:

type TestEvents = {
  foo: (payload: { bar: string }) => void
}

class TestEmitter extends EventEmitter<TestEvents> {
  test() {
    this.emit('foo', { bar: 'pizza' })
  }
}

So it seems like it should be possible for the return value of eventPromise to be strongly typed:

const testEmitter = new TestEmitter()
const { bar } = await eventPromise(testEmitter, 'foo') 

But this gives me an error, Property 'bar' does not exist on type 'unknown'. (See this playground.)

I've gone around in circles quite a bit trying to get eventPromise to return a strongly typed value. This is the closest I've gotten:

export const eventPromise = async <
  T extends EventMap,
  K extends EventEmitter.EventNames<T> = EventEmitter.EventNames<T>,
>(
  emitter: EventEmitter<T>,
  event: K
) => {
  return new Promise<Parameters<T[K]>[0]>(resolve => {
    const listener = (payload: Parameters<T[K]>[0]) => resolve(payload)
    emitter.once(event, listener as EventEmitter.EventListener<T, K>)
  })
}

This compiles, but the return type is still unknown. (See this playground.)

I honestly feel like none of these gymnastics should even be necessary - shouldn't the original function have enough information for TypeScript to infer its return type?

What am I missing?


Clarification: This should work generically with any EventEmitter, not just one that handles TestEvents.

3

There are 3 answers

11
Julio Di Egidio -- inactive On

I was able to make it work with the code below.

I have extracted a definition of TestMessages more for convenience, the relevant differences with the code you have shown are in eventPromise, namely:

  • I am actually returning from the promise callback;
  • I have given a type to the returned promise.

eventPromise could be abstracted further in order to take emitters with arbitrary events in input, so it should become generic, but whether that is even needed already depends on specific requirements.

import { EventEmitter } from 'eventemitter3'

type TestMessages = {
    foo: { bar: string }
}

type TestEvents = {
    foo: (payload: TestMessages["foo"]) => void
}

class TestEmitter extends EventEmitter<TestEvents> {
    test() {
        this.emit('foo', { bar: 'pizza' })
    }
}

/** Returns a promise that resolves when the given event fires */
export const eventPromise = async (emitter: TestEmitter, event: keyof TestEvents) =>
    new Promise<TestMessages[typeof event]>(resolve => {
        return emitter.once(event, d => resolve(d))
    });

const testEmitter = new TestEmitter();

const { bar } = await eventPromise(testEmitter, 'foo');

Link to playground

1
Man Au On

Extending on @Julio Di Egidio's answer, you can use the Parameters utility type to avoid creating another type.

import ValidEventTypes, { EventEmitter } from 'eventemitter3'

/** Returns a promise that resolves when the given event fires */
async function eventPromise<T extends ValidEventTypes, U extends keyof 
TestEvents>(emitter: T, event: U) {
  const res = new Promise<Parameters<TestEvents[U]>[0]>(resolve => {
    return emitter.once(event, (d) => resolve(d))
  })
  return res
}

type TestEvents = {
   foo: (payload: { bar: string }) => void
}

class TestEmitter extends EventEmitter<TestEvents> {
  test() {
    this.emit('foo', { bar: 'pizza' })
  }
}

const testEmitter = new TestEmitter()
const { bar } = await eventPromise(testEmitter, 'foo')
3
Oscar On

Maybe I'm wrong, but regarding official documentation it seems that Parameters will always return the unknow type when passed a generic type. Reason why it is not possible to get exact type for eventPromise() function by this way.enter image description here