2.0.0 • Published 5 years ago

events-typed v2.0.0

Weekly downloads
1
License
MIT
Repository
github
Last release
5 years ago

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

(playground example)

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

(playground example)

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 of src/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?)
2.0.0

5 years ago

1.1.1

5 years ago

1.1.0

5 years ago

1.0.4

5 years ago

1.0.3

5 years ago

1.0.2

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago

0.0.0

5 years ago