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 –
'pending'
, the eventHandler
receives no argument'success'
, the eventHandler
receives a number
'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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With