events-typed
v2.0.0
Published
A typesafe EventEmitter for TypeScript that wraps Node.js EventEmitter.
Downloads
24
Maintainers
Readme
events.ts
A typesafe EventEmitter for TypeScript that wraps Node.js EventEmitter.
Works in browser environments too.
This gives you the same EventEmitter
from Node, wrapped, so the API is
almost the same. As usual, any
can be used as an escape hatch (see Caveats
and TODO below).
Why?
When using @types/node
, event names can be any string and are not checked
against a known list of event names, and event payloads all have type any
.
Because of this you can not enforce strict typing with Node.js EventEmitter. :(
The advantage of this package over @types/node
is that event names are checked
against a list of known event names that you define (otherwise you get a type
error if you provide an invalid event name) and event payloads all have types
that you define (and you'll get a type error if you pass a callback that doesn't
have a signature that accepts the payload type).
Usage
It's easiest to explain with code. event.ts
lets you do the following:
import { makeEventEmitterClass } from 'events.ts'
// define the event names and their payloads:
type EventTypes = {
SOME_EVENT: number
OTHER_EVENT: string
ANOTHER_EVENT: undefined
READONLY_EVENT: ReadonlyArray<number>
}
// parens required, because EventEmitter is a factory that returns a class
const EventEmitter = makeEventEmitterClass<EventTypes>()
const emitter = new EventEmitter()
// GOOD --------------- :
emitter.on('SOME_EVENT', payload => testString(payload))
emitter.on('OTHER_EVENT', payload => testString(payload))
emitter.on('ANOTHER_EVENT', (/* no payload */) => {})
emitter.on('READONLY_EVENT', payload => {
testReadonlyArray(payload)
})
emitter.emit('SOME_EVENT', 42)
emitter.emit('OTHER_EVENT', 'foo')
emitter.emit('ANOTHER_EVENT')
emitter.emit('READONLY_EVENT', Object.freeze([1, 2, 3]))
// BAD --------------- :
emitter.on('SOME_EVENT', payload => testString(payload)) // ERROR: Argument of type 'number' is not assignable to parameter of type 'string'.
emitter.on('OTHER_EVENT', payload => testNumber(payload)) // ERROR: Argument of type 'string' is not assignable to parameter of type 'number'.
emitter.on('ANOTHER_EVENT', (payload: number) => {}) // ERROR: Argument of type '(payload: number) => void' is not assignable to parameter of type '() => void'.
emitter.on('READONLY_EVENT', (payload: number) => {
testNumber(payload) // ERROR, payload parameter is not ReadonlyArray<number>
})
emitter.emit('foo', 123) // ERROR: Argument of type '"FOOBAR"' is not assignable to parameter of type '"SOME_EVENT" | "OTHER_EVENT" | "ANOTHER_EVENT" | "READONLY_EVENT"'.
emitter.emit('SOME_EVENT', 'foo') // ERROR: Argument of type '"foo"' is not assignable to parameter of type 'number'.
emitter.emit('OTHER_EVENT', 42) // ERROR: Argument of type '42' is not assignable to parameter of type 'string'.
emitter.emit('ANOTHER_EVENT', 'bar') // ERROR: Expected 1 arguments, but got 2.
emitter.emit('READONLY_EVENT', ['1', '2', '3']) // ERROR: Type 'string' is not assignable to type 'number'.
declare function testNumber(value: number): void
declare function testString(value: string): void
declare function testReadonlyArray(value: ReadonlyArray<number>): void
Here's the a simple playground example showing the concept (with a subset of the EventEmitter API).
You might be accustomed to using enums for event names, for example something like the following so that you perhaps get better autocompletion:
emitter.emit(Events.SOME_EVENT, payload)
One way you can achieve this is to also write an enum alongside your event types:
import { makeEventEmitterClass } from 'events.ts'
// define the event names and their payloads:
type EventTypes = {
SOME_EVENT: number
OTHER_EVENT: string
ANOTHER_EVENT: undefined
READONLY_EVENT: ReadonlyArray<number>
}
// define an enum so we don't have to pass string literals into API calls
enum Events = {
SOME_EVENT: number
OTHER_EVENT: string
ANOTHER_EVENT: undefined
READONLY_EVENT: ReadonlyArray<number>
}
// parens required, because EventEmitter is a factory that returns a class
const EventEmitter = makeEventEmitterClass<EventTypes>()
const emitter = new EventEmitter()
emitter.emit(Events.SOME_EVENT, 42) // autocompletion works well
But you may notice that if the list of event names gets long, that you'll now
have two lists of events: one for the types, and one for the enum. You might
rather have things be DRY and only mention each event name exactly once instead
of mentioning each event name three times. There a way to do that using a
class
hack. The above example becomes:
import { makeEventEmitterClass } from 'events.ts'
// define all event names and types in a class as constructor args:
class EventTypes {
constructor(
public SOME_EVENT: number,
public OTHER_EVENT: string,
public ANOTHER_EVENT: undefined,
public READONLY_EVENT: ReadonlyArray<number>,
) {}
}
// Make an empty Events object, which will be like an enum
const Events = {} as { [k in keyof EventTypes]: k }
// loop on the keys of a dummy EventTypes instance in order to create the
// enum-like Events object keys.
for (const key in new (EventTypes as any)()) {
Events[key] = key
}
// parens required, because EventEmitter is a factory that returns a class
const EventEmitter = makeEventEmitterClass<EventTypes>()
const emitter = new EventEmitter()
emitter.emit(Events.SOME_EVENT, 42) // autocompletion works well
Caveats
Due to how TypeScript works, it is not possible to implement this feature as strictly a type declaration file. It requires runtime output in the form of a
class
, and thus can not be simply merged into@types/node
. See the bottom ofsrc/index.ts
to see the part that is required and which emits the runtime code.To keep things DRY, you will have to make a dummy class hack like in the last example.
Symbols for event names are not currently supported.
Multiple event payload arguments are not currently supported.
The following,
emitter.emit('foo', singleArg)
is supported, but
emitter.emit('foo', arg1, arg2, arg3)
is not yet possible.
TODO
- [ ] Support Symbol event names (how?)
- [ ] Support multiple event payload args (how?)