Limiting the type of `eventName` in a class that extends EventEmitter with Flow?
Asked Answered
P

1

8

As an example, let's say I have a class that only emits three possible events – 'pending' or 'success' or 'failure'. Additionally, the type of the argument received in the eventHandler depends on which event was emitted –

  • if 'pending', the eventHandler receives no argument
  • if 'success', the eventHandler receives a number
  • if 'failure', the eventHandler receives an Error

Here is how I tried to model that:

// @flow

import EventEmitter from 'events'

type CustomEventObj = {|
  pending: void,
  success: number,
  error: Error
|}

declare class MyEventEmitter extends EventEmitter {
  on<K: $Keys<CustomEventObj>>(
    eventName: K,
    eventHandler: (
      e: $ElementType<CustomEventObj, K>, 
      ...args: Array<any>
    ) => void
  ): this
}

However, this results in an error like so:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ test.js:12:3

Cannot extend EventEmitter [1] with MyEventEmitter because an indexer property is missing in CustomEventObj [2] in the
first argument of property on.

 [1]  3│ import EventEmitter from 'events'
       :
      8│   error: Error
      9│ |}
     10│
     11│ declare class MyEventEmitter extends EventEmitter {
 [2] 12│   on<K: $Keys<CustomEventObj>>(
     13│     eventName: K,
     14│     eventHandler: (
     15│       e: $ElementType<CustomEventObj, K>, 
     16│       ...args: Array<any>
     17│     ) => void
     18│   ): this
     19│ }
     20│

I don't want to have an indexer property on CustomEventObj because wouldn't that kill the point of only having 3 possible events?

Any help would be appreciated.

Peculiar answered 21/3, 2018 at 16:17 Comment(0)
I
3

Well, I've got good news and bad news. The good news is, you will be able to get Flow to understand what's being passed as an argument to the on handler. The bad news is, you won't be able to tell Flow that other events outside of your desired events are not allowed.

Because you're extending the EventEmitter class, you're explicitly stating that your class will do the same sorts of things that EventEmitter does. As a result, because the EventEmitter's on handler will take in any string, your "MyEventEmitter" must also accept any string in the on handler. Even if you just throw an error if/when you get an unexpected string, Flow still expects you to follow that contract.

Now for the good news: you can let Flow know what kind of arguments you're expecting to receive on certain events. The trick is to specify multiple declarations of the on handler, like so:

// @flow

import EventEmitter from 'events'

type CustomEventObj = {|
  pending: void,
  success: number,
  error: Error
|}

declare class MyEventEmitter extends EventEmitter {
  on(
    'pending',
    // Alternatively, you can pass in a callback like () => void
    // as the type here. Either works for the examples below.
    (
      e: null,
      ...args: Array<any>
    ) => void
  ): this;

  on(
    'success',
    (
      e: number,
      ...args: Array<any>
    ) => void
  ): this;

  on(
    'error',
    (
      e: Error,
      ...args: Array<any>
    ) => void
  ): this;

  // Just to satisfy inheritance requirements, but this should
  // throw at runtime.
  on(
    string,
    (
      e: mixed,
      ...args: Array<any>
    ) => void
  ): this;
}

let myInstance = new MyEventEmitter()

myInstance.on('pending', () => {}) // works
myInstance.on('success', (num: number) => {console.log(num * 2)}) // works
myInstance.on('success', (badStr: string) => {console.log(badStr.length)}) // Error

Now flow understands that if the event is "success", for example, then the callback should accept a number (or some union type that includes number like string | number or any).

If you want Flow to only allow for you to pass in specific events, then you won't be able to inherit from the EventEmitter class. (The reasoning goes like this: what if you passed your MyEventEmitter to some external code that passed other events? Flow would have no way to prevent that from happening, and the external code would have no good way of knowing it can't pass in events besides 'pending', 'success' or 'failure')

I've posted a copy of my code on GitHub for trying/reference/forking. I'd post it on https://flow.org/try/, but I can't seem to get it to understand the inheritance from either EventEmitter or events$EventEmitter.

Caveat: I think the reason you can specify your expected arguments to the function is because the events$EventEmitter's on function has the handler parameter declared as Function type. This type is a little weird and mostly unchecked. So while this technique of multiple declarations on the on handler works for now (Flow 0.69.0), it may break with future versions of Flow or the events$EventsEmitter typedef.

Ill answered 30/3, 2018 at 2:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.