Installations
npm install strongly-typed-event-emitter
Developer Guide
Typescript
Yes
Module System
CommonJS
Node Version
10.15.3
NPM Version
6.9.0
Score
64.4
Supply Chain
96.4
Quality
75.1
Maintenance
100
Vulnerability
100
License
Releases
Unable to fetch releases
Contributors
Unable to fetch Contributors
Languages
TypeScript (99.63%)
JavaScript (0.37%)
Developer
Download Statistics
Total Downloads
622
Last Day
1
Last Week
4
Last Month
11
Last Year
86
GitHub Statistics
4 Stars
54 Commits
1 Forks
2 Watching
1 Branches
1 Contributors
Bundle Size
304.00 B
Minified
227.00 B
Minified + Gzipped
Package Meta Information
Latest Version
0.0.2
Package Id
strongly-typed-event-emitter@0.0.2
Unpacked Size
17.79 kB
Size
5.05 kB
File Count
7
NPM Version
6.9.0
Node Version
10.15.3
Total Downloads
Cumulative downloads
Total Downloads
622
Last day
0%
1
Compared to previous day
Last week
0%
4
Compared to previous week
Last month
83.3%
11
Compared to previous month
Last year
-11.3%
86
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
@g-rath/strongly-typed-event-emitter
This is a strongly typed version of the Node 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:
1import { api, btns } from 'awesome-app'; 2import { EventEmitter } from 'events'; 3 4const ee = new EventEmitter(); 5 6ee.on('e:user.login', data => { 7 const token = data.tokne; 8 9 api.setAuthToken(token); 10}); 11 12ee.on('e:user.signout', data => { 13 console.log(data.user.firstName, 'has signed out'); 14}); 15 16btns.logout.onclick = () => { 17 ee.emit('e:user.logout', {}); 18};
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):
1import { api, btns } from 'awesome-app'; 2import { EventEmitter } from 'events'; 3import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter'; 4 5interface UserEventsMap { 6 'e:user.login': { token: string }; 7 'e:user.logout': { user: { firstName: string } }; 8} 9 10// note this can also be done as "const ee = new StronglyTypedEventEmitter<UserEventsMap>();" 11const ee: StronglyTypedEventEmitter<UserEventsMap> = new EventEmitter(); 12 13ee.on('e:user.login', data => { 14 // TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'? 15 const token = data.tokne; 16 17 api.setAuthToken(token); 18}); 19 20// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type '"e:user.login" | "e:user.logout"'. 21ee.on('e:user.signout', data => { 22 console.log(data.user.firstName, 'has signed out'); 23}); 24 25btns.logout.onclick = () => { 26 // TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'. 27 ee.emit('e:user.logout', {}); 28};
There are two caveats with this package:
- You must pass a value to
emit
, even if that value isundefined
. - 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:
1import { EventEmitter } from 'events'; 2import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter'; 3 4const ee1: StronglyTypedEventEmitter<{}> = new EventEmitter(); 5const ee2 = new StronglyTypedEventEmitter<{}>();
The syntax for the generic event map is Record<PropertyKey, unknown>
.
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.
1import { api, btns } from 'awesome-app'; 2import { EventEmitter } from 'events'; 3import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter'; 4 5enum UserEvent { 6 Login = 'e:user.login', 7 Logout = 'e:user.logout' 8} 9 10interface UserEventsMap { 11 [UserEvent.Login]: { token: string }; 12 [UserEvent.Logout]: { user: { firstName: string } }; 13} 14 15// note this can also be done as "const ee = new StronglyTypedEventEmitter<UserEventsMap>();" 16const ee: StronglyTypedEventEmitter<UserEventsMap> = new EventEmitter(); 17 18ee.on(UserEvent.Login, data => { 19 // TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'? 20 const token = data.tokne; 21 22 api.setAuthToken(token); 23}); 24 25// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type 'UserEvent'. 26ee.on('e:user.signout', data => { 27 console.log(data.user.firstName, 'has signed out'); 28}); 29 30btns.logout.onclick = () => { 31 // TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'. 32 ee.emit(UserEvent.Logout, {}); 33};
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):
1import { EventEmitter } from 'events'; 2import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter'; 3 4enum SocketEvent { 5 Heartbeat = 'e:heartbeat' 6} 7 8interface SocketEventsMap { 9 [SocketEvent.Heartbeat]: void; 10} 11 12// note this can also be done as "const ee = new StronglyTypedEventEmitter<SocketEventsMap>();" 13const ee: StronglyTypedEventEmitter<SocketEventsMap> = new EventEmitter(); 14 15// TS2554: Expected 2 arguments, but got 1. 16ee.emit(SocketEvent.Heartbeat); 17// TS2345: Argument of type '{}' is not assignable to parameter of type 'void'. 18ee.emit(SocketEvent.Heartbeat, {}); 19 20ee.emit(SocketEvent.Heartbeat, undefined);
Merging event maps
Merging works just fine too!
1import { api, btns } from 'awesome-app'; 2import { EventEmitter } from 'events'; 3import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter'; 4 5enum UserEvent { 6 Login = 'e:user.login', 7 Logout = 'e:user.logout' 8} 9 10interface UserAuthEventsMap { 11 [UserEvent.Login]: { token: string }; 12 [UserEvent.Logout]: { user: { firstName: string } }; 13} 14 15interface UserProfileEventsMap { 16 'e:user.save': { user: { firstName: string } }; 17} 18 19// note this can also be done as "const ee = new StronglyTypedEventEmitter<UserEventsMap & UserProfileEventsMap>();" 20const ee: StronglyTypedEventEmitter<UserAuthEventsMap & UserProfileEventsMap> = new EventEmitter(); 21 22ee.on(UserEvent.Login, data => { 23 // TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'? 24 const token = data.tokne; 25 26 api.setAuthToken(token); 27}); 28 29// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type 'UserEvent | "e:user.save"'. 30ee.on('e:user.signout', data => { 31 console.log(data.user.firstName, 'has signed out'); 32}); 33 34btns.save.onclick = () => { 35 // TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'. 36 ee.emit('e:user.save', {}); 37};
You can even merge events with the same key:
1import { EventEmitter } from 'events'; 2import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter'; 3 4interface UserEventsMap { 5 'e:user.save': { 6 user: { 7 firstName: string; 8 lastName: string; 9 }; 10 }; 11} 12 13interface AdminEventsMap { 14 'e:user.save': { 15 user: { username: string; }; 16 roles: string[]; 17 }; 18} 19 20// note this can also be done as "const ee = new StronglyTypedEventEmitter<UserEventsMap & AdminEventsMap>();" 21const ee: StronglyTypedEventEmitter<UserEventsMap & AdminEventsMap> = new EventEmitter(); 22 23ee.on('e:user.save', data => { 24 console.log( 25 data.user.username, 26 data.user.firstName, 27 data.user.lastName, 28 data.roles 29 ); 30});
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:
1import { api, btns } from 'awesome-app'; 2import { StronglyTypedEventEmitter } from 'strongly-typed-event-emitter'; 3 4enum UserEvent { 5 Login = 'e:user.login', 6 Logout = 'e:user.logout' 7} 8 9interface UserAuthEventsMap { 10 [UserEvent.Login]: { token: string }; 11 [UserEvent.Logout]: { user: { firstName: string } }; 12} 13 14class UserManager extends StronglyTypedEventEmitter<UserAuthEventsMap> { 15 16} 17 18const ee = new UserManager(); 19 20ee.on(UserEvent.Login, data => { 21 // TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'? 22 const token = data.tokne; 23 24 api.setAuthToken(token); 25}); 26 27// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type 'UserEvent'. 28ee.on('e:user.signout', data => { 29 console.log(data.user.firstName, 'has signed out'); 30}); 31 32btns.logout.onclick = () => { 33 // TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'. 34 ee.emit(UserEvent.Logout, {}); 35};
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:
1import { api, btns } from 'awesome-app'; 2import { StronglyTypedEventEmitter, EventMap } from 'strongly-typed-event-emitter'; 3 4enum UserEvent { 5 Login = 'e:user.login', 6 Logout = 'e:user.logout' 7} 8 9interface UserAuthEventsMap { 10 [UserEvent.Login]: { token: string }; 11 [UserEvent.Logout]: { user: { firstName: string } }; 12} 13 14class UserAuthManager<T extends EventMap = {}> extends StronglyTypedEventEmitter<UserAuthEventsMap & T> { 15 16} 17 18interface UserProfileEventsMap { 19 'e:user.save': { user: { firstName: string } }; 20} 21 22class UserManager<T extends EventMap = {}> extends UserAuthManager<UserProfileEventsMap & T> { 23 24} 25 26const ee = new UserManager(); 27 28ee.on(UserEvent.Login, data => { 29 // TS2551: Property 'tokne' does not exist on type '{ token: string; }'. Did you mean 'token'? 30 const token = data.tokne; 31 32 api.setAuthToken(token); 33}); 34 35// TS2345: Argument of type '"e:user.signout"' is not assignable to parameter of type 'UserEvent | "e:user.save"'. 36ee.on('e:user.signout', data => { 37 console.log(data.user.firstName, 'has signed out'); 38}); 39 40btns.save.onclick = () => { 41 // TS2345: Argument of type '{}' is not assignable to parameter of type '{ user: { firstName: string; }; }'. 42 ee.emit('e:user.save', {}); 43};
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 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.
No vulnerabilities found.
Reason
no binaries found in the repo
Reason
0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Reason
Found 0/12 approved changesets -- score normalized to 0
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
security policy file not detected
Details
- Warn: no security policy file detected
- Warn: no security file to analyze
- Warn: no security file to analyze
- Warn: no security file to analyze
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
license file not detected
Details
- Warn: project does not have a license file
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 20 are checked with a SAST tool
Reason
58 existing vulnerabilities detected
Details
- Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92
- Warn: Project is vulnerable to: GHSA-6chw-6frg-f759
- Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw
- Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw
- Warn: Project is vulnerable to: GHSA-fwr7-v2mv-hh25
- Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg
- Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275
- Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c
- Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq
- Warn: Project is vulnerable to: GHSA-3gx7-xhv7-5mx3
- Warn: Project is vulnerable to: GHSA-8r6j-v8pm-fqw3
- Warn: Project is vulnerable to: MAL-2023-462
- Warn: Project is vulnerable to: GHSA-w457-6q6x-cgp9
- Warn: Project is vulnerable to: GHSA-62gr-4qp9-h98f
- Warn: Project is vulnerable to: GHSA-f52g-6jhx-586p
- Warn: Project is vulnerable to: GHSA-2cf5-4w76-r9qv
- Warn: Project is vulnerable to: GHSA-3cqr-58rm-57f8
- Warn: Project is vulnerable to: GHSA-g9r4-xpmj-mj65
- Warn: Project is vulnerable to: GHSA-q2c6-c6pm-g3gh
- Warn: Project is vulnerable to: GHSA-765h-qjxv-5f44
- Warn: Project is vulnerable to: GHSA-f2jv-r9rf-7988
- Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj
- Warn: Project is vulnerable to: GHSA-qqgx-2p2h-9c37
- Warn: Project is vulnerable to: GHSA-896r-f27r-55mw
- Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h
- Warn: Project is vulnerable to: GHSA-6c8f-qphg-qjgp
- Warn: Project is vulnerable to: GHSA-p6mc-m468-83gw
- Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9
- Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm
- Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv
- Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3
- Warn: Project is vulnerable to: GHSA-vh95-rmgr-6w4m
- Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h
- Warn: Project is vulnerable to: GHSA-fhjf-83wg-r2j9
- Warn: Project is vulnerable to: GHSA-5fw9-fq32-wv5p
- Warn: Project is vulnerable to: GHSA-hj48-42vr-x3v9
- Warn: Project is vulnerable to: GHSA-6fw4-hr69-g3rv
- Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp
- Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6
- Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw
- Warn: Project is vulnerable to: GHSA-4g88-fppr-53pp
- Warn: Project is vulnerable to: GHSA-4jqc-8m5r-9rpr
- Warn: Project is vulnerable to: GHSA-3f95-r44v-8mrg
- Warn: Project is vulnerable to: GHSA-28xr-mwxg-3qc8
- Warn: Project is vulnerable to: GHSA-9p95-fxvg-qgq2
- Warn: Project is vulnerable to: GHSA-9w5j-4mwv-2wj8
- Warn: Project is vulnerable to: GHSA-3jfq-g458-7qm9
- Warn: Project is vulnerable to: GHSA-r628-mhmh-qjhw
- Warn: Project is vulnerable to: GHSA-9r2w-394v-53qc
- Warn: Project is vulnerable to: GHSA-5955-9wpr-37jh
- Warn: Project is vulnerable to: GHSA-qq89-hq3f-393p
- Warn: Project is vulnerable to: GHSA-f5x3-32g6-xq36
- Warn: Project is vulnerable to: GHSA-jgrx-mgxx-jf9v
- Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3
- Warn: Project is vulnerable to: GHSA-6fc8-4gx4-v693
- Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q
- Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh
- Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp
Score
1.5
/10
Last Scanned on 2025-02-03
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn MoreOther packages similar to strongly-typed-event-emitter
@akolos/event-emitter
A strongly-typed Node-style event emitter written in TypeScript.
@siteimprove/alfa-emitter
An implementation of a strongly typed and asynchronously iterable event emitter
typed-event-dispatcher
Strongly-typed events that can be publicly listened but internally-only dispatched.
eventin
Strongly-typed event emitter.