https://github.com/g-rath/strongly-typed-event-emitter
A strongly typed version of the Node EventEmitter.
https://github.com/g-rath/strongly-typed-event-emitter
event-emitter eventemitter node strongly-typed typescript
Last synced: 7 months ago
JSON representation
A strongly typed version of the Node EventEmitter.
- Host: GitHub
- URL: https://github.com/g-rath/strongly-typed-event-emitter
- Owner: G-Rath
- Created: 2019-04-12T23:01:06.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2019-07-13T08:01:12.000Z (about 6 years ago)
- Last Synced: 2025-02-28T16:56:46.023Z (7 months ago)
- Topics: event-emitter, eventemitter, node, strongly-typed, typescript
- Language: TypeScript
- Homepage: https://nodejs.org/api/events.html#events_class_eventemitter
- Size: 421 KB
- Stars: 4
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# strongly-typed-event-emitter
This is a strongly typed version of the Node [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter).
## The Problem
Node provides an `EventEmitter` class, that is great for doing custom event emitting.
It's sweet and simple, providing exactly what you need and saving you from an extra package, or having to manage a custom implementation.The problem however is that this class is weakly typed with `any` - This is understandable since there is no way to know anything about the users events:
```typescript
import { api, btns } from 'awesome-app';
import { EventEmitter } from 'events';const ee = new EventEmitter();
ee.on('e:user.login', data => {
const token = data.tokne;api.setAuthToken(token);
});ee.on('e:user.signout', data => {
console.log(data.user.firstName, 'has signed out');
});btns.logout.onclick = () => {
ee.emit('e:user.logout', {});
};
```The example above highlights how using `EventEmitter`s in `TypeScript` can let a number of bugs slip by:
* You don't get warned if you use properties that don't exist (like `tokne`)
* This can be mitigated by annotating the parameters type, but still annoying, since you have to track down the type.
* When renaming or removing events, you won't get told of where they're used, and so risk not catching every usage in your app.
* When emitting an event, you'll not get any warning about missing properties on the data parameter.On top of this, you'll also not get autocompleting anywhere near `EventEmitter`s, leaving you to run around tracking types & event keys down.
Finally, while you could try and solve this with inheritance, you'll run into overloading problems that'll mean `TypeScript` either errors, still doesn't enforce strong types,
or require some really hack-y typings that will likely cause IDEs (and maybe even `TypeScript) to bail out, ruining any chance of autocompletion.## The Solution
Enter `StronglyTypedEventEmitter`, a strongly typed version of the `EventEmitter`. This declared class takes a `Record` that maps event keys to their emitted data as a generic parameter,
This is actually just a re-declaration of the Node `EventEmitter`, meaning at runtime there is no extra overhead - it's the same as if you'd used `EventEmitter` directly.
In addition to providing a type, this package also provides a re-export of the Node `EventEmitter` as `StronglyTypedEventEmitter`, letting you use inheritance
while still not incurring a runtime cost (aside from the extra import - more on this in the inheritance examples):```typescript
import { api, btns } from 'awesome-app';
import { EventEmitter } from 'events';
import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter';interface UserEventsMap {
'e:user.login': { token: string };
'e:user.logout': { user: { firstName: string } };
}// note this can also be done as "const ee = new StronglyTypedEventEmitter();"
const ee: StronglyTypedEventEmitter = new EventEmitter();ee.on('e:user.login', data => {
// TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'?
const token = data.tokne;api.setAuthToken(token);
});// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type '"e:user.login" | "e:user.logout"'.
ee.on('e:user.signout', data => {
console.log(data.user.firstName, 'has signed out');
});btns.logout.onclick = () => {
// TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'.
ee.emit('e:user.logout', {});
};
```There are two caveats with this package:
1. You must pass a value to `emit`, even if that value is `undefined`.
2. You can only have one "event" parameter - `...args` is not supported.## Usage:
You can create a strongly typed emitter by using either a type annotation, or calling `new` on `StronglyTypedEventEmitter` directly:
```typescript
import { EventEmitter } from 'events';
import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter';
const ee1: StronglyTypedEventEmitter<{}> = new EventEmitter();
const ee2 = new StronglyTypedEventEmitter<{}>();
```The syntax for the generic event map is `Record`.
Full autocompleting in IDEs such as `WebStorm` are expected work, along with type checking by `TypeScript`.
If you're having problems or unexpected results with either of these features, please make an issue on this repo.Here are some examples of `StronglyTypedEventEmitter` in action:
### Enums as keys
You can use enums as keys just fine. Note that this will lock you into using the enum; you can't pass the value of a key of the enum.
This isn't strictly a bad thing, but it means you *must* export your enum if you want to use it outside of the file its in.```typescript
import { api, btns } from 'awesome-app';
import { EventEmitter } from 'events';
import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter';enum UserEvent {
Login = 'e:user.login',
Logout = 'e:user.logout'
}interface UserEventsMap {
[UserEvent.Login]: { token: string };
[UserEvent.Logout]: { user: { firstName: string } };
}// note this can also be done as "const ee = new StronglyTypedEventEmitter();"
const ee: StronglyTypedEventEmitter = new EventEmitter();ee.on(UserEvent.Login, data => {
// TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'?
const token = data.tokne;api.setAuthToken(token);
});// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type 'UserEvent'.
ee.on('e:user.signout', data => {
console.log(data.user.firstName, 'has signed out');
});btns.logout.onclick = () => {
// TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'.
ee.emit(UserEvent.Logout, {});
};
```### Events that don't have `data` (Caveat #1)
This is the first caveat of this package - if you have an event with no `data`,
you still have to pass a second parameter to `emit` (and other such functions):```typescript
import { EventEmitter } from 'events';
import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter';enum SocketEvent {
Heartbeat = 'e:heartbeat'
}interface SocketEventsMap {
[SocketEvent.Heartbeat]: void;
}// note this can also be done as "const ee = new StronglyTypedEventEmitter();"
const ee: StronglyTypedEventEmitter = new EventEmitter();// TS2554: Expected 2 arguments, but got 1.
ee.emit(SocketEvent.Heartbeat);
// TS2345: Argument of type '{}' is not assignable to parameter of type 'void'.
ee.emit(SocketEvent.Heartbeat, {});ee.emit(SocketEvent.Heartbeat, undefined);
```### Merging event maps
Merging works just fine too!
```typescript
import { api, btns } from 'awesome-app';
import { EventEmitter } from 'events';
import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter';enum UserEvent {
Login = 'e:user.login',
Logout = 'e:user.logout'
}interface UserAuthEventsMap {
[UserEvent.Login]: { token: string };
[UserEvent.Logout]: { user: { firstName: string } };
}interface UserProfileEventsMap {
'e:user.save': { user: { firstName: string } };
}// note this can also be done as "const ee = new StronglyTypedEventEmitter();"
const ee: StronglyTypedEventEmitter = new EventEmitter();ee.on(UserEvent.Login, data => {
// TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'?
const token = data.tokne;api.setAuthToken(token);
});// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type 'UserEvent | "e:user.save"'.
ee.on('e:user.signout', data => {
console.log(data.user.firstName, 'has signed out');
});btns.save.onclick = () => {
// TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'.
ee.emit('e:user.save', {});
};
```You can even merge events with the same key:
```typescript
import { EventEmitter } from 'events';
import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter';interface UserEventsMap {
'e:user.save': {
user: {
firstName: string;
lastName: string;
};
};
}interface AdminEventsMap {
'e:user.save': {
user: { username: string; };
roles: string[];
};
}// note this can also be done as "const ee = new StronglyTypedEventEmitter();"
const ee: StronglyTypedEventEmitter = new EventEmitter();ee.on('e:user.save', data => {
console.log(
data.user.username,
data.user.firstName,
data.user.lastName,
data.roles
);
});
```You should be careful while doing this however - don't mistake the result for a union.
### Inheritance
Finally, you can extend from `StronglyTypedEventEmitter` just fine:
```typescript
import { api, btns } from 'awesome-app';
import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter';enum UserEvent {
Login = 'e:user.login',
Logout = 'e:user.logout'
}interface UserAuthEventsMap {
[UserEvent.Login]: { token: string };
[UserEvent.Logout]: { user: { firstName: string } };
}class UserManager extends StronglyTypedEventEmitter {
}
const ee = new UserManager();
ee.on(UserEvent.Login, data => {
// TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'?
const token = data.tokne;api.setAuthToken(token);
});// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type 'UserEvent'.
ee.on('e:user.signout', data => {
console.log(data.user.firstName, 'has signed out');
});btns.logout.onclick = () => {
// TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'.
ee.emit(UserEvent.Logout, {});
};
```### 2 degrees of inheritance
If you're using inheritance, it's recommended that you add an optional generic parameter to your class,
that is merged into `StronglyTypedEventEmitter`.That way, if anyone extends your class, they can add their own events:
```typescript
import { api, btns } from 'awesome-app';
import { StronglyTypedEventEmitter, EventMap } from 'strongly-typed-event-emitter';enum UserEvent {
Login = 'e:user.login',
Logout = 'e:user.logout'
}interface UserAuthEventsMap {
[UserEvent.Login]: { token: string };
[UserEvent.Logout]: { user: { firstName: string } };
}class UserAuthManager extends StronglyTypedEventEmitter {
}
interface UserProfileEventsMap {
'e:user.save': { user: { firstName: string } };
}class UserManager extends UserAuthManager {
}
const ee = new UserManager();
ee.on(UserEvent.Login, data => {
// TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'?
const token = data.tokne;api.setAuthToken(token);
});// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type 'UserEvent | "e:user.save"'.
ee.on('e:user.signout', data => {
console.log(data.user.firstName, 'has signed out');
});btns.save.onclick = () => {
// TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'.
ee.emit('e:user.save', {});
};
```## Contributing
The most important thing when contributing is to make sure to add information about changes to the `CHANGELOG.md`,
ideally before publishing a new version. If you're not confident doing this, just ensure you provide primary maintainers
as much information as possible, particular about any special rules or gotchas that are a result of your change.#### Linting
To run `eslint` on the project, run:
```
npm run lint
```#### Testing
There is no real way to test this kind of package - instead, jest snapshots are used
to ensure all changes that are made result in a known (and therefore expected) reaction from TypeScript.These snapshots are based off the code examples in this README.
Whenever a change is made, these snapshot tests should be run, and updated as needed.
To run `jest` on the project, run:
```
npm run test
```#### Checking
To check that the project is type safe, run:
```
npm run check
```#### Compiling
To compile the project using `TypeScript`, run:
```
npm run compile
```#### Changelog
This package uses a `CHANGELOG.md` to track, note, and describe changes to its surface.
All documentable changes should be, being placed under the appropriate header in the `CHANGELOG`.
Note that the `CHANGELOG` is *not* fixed - it's perfectly reasonable to edit it after the fact, for whatever reason.
The version headers of the `CHANGELOG` are automated by an `npm-version` script, located in the `scripts` folder,
When run, the script will insert a new version header below the `[Unreleased]` header.The version header is enclosed in a link, linking to the comparing page for the repo
(to allow users to easily bring up a full git comparision between the new & previous versions of the package),
and has the date of the release at the end.#### Tagging, Versioning & Publishing
We use [SemVer](http://semver.org/) for versioning.
Tags should match the release versions, with a prefixing `v`
Both publishing & versioning should be done using `npm`, which'll also handle tags.
To publish a new version of this package, use `npm publish`.
There is an `npm-version` script located in the `scripts` folder of the repo,
that handles keeping the `CHANGELOG` headers in sync with new package versions.