A pure TypeScript, zero-dependency, type-safe event emitter for Deno, Node.js, Bun, and browsers.
MightyEmitter is a lightweight, fully typed event emitter built from the ground up in TypeScript. It delivers compile-time safety for event names and payloads while staying small enough for any project — from CLI tools to frontend apps.
| Feature | MightyEmitter | Node EventEmitter | mitt | EventEmitter3 |
|---|---|---|---|---|
| Full type safety | Yes | No | Partial | No |
| Zero dependencies | Yes | Yes | Yes | Yes |
async / await support (next) |
Yes | No | No | No |
Async iteration (iter) |
Yes | No | No | No |
| AbortSignal support | Yes | No | No | No |
| Works in Deno, Node, Bun, & browsers | Yes | Node only | Yes | Yes |
- Type-safe — event names and payloads are checked at compile time via generics. No more silent typos.
- Zero dependencies — nothing to audit, nothing to break.
- Tiny footprint — a single file, under 5 KB unminified.
- Async-first —
next()returns a promise for the next event;iter()gives you anAsyncIterableIteratorfor streaming consumption. - Cancellable — both
next()anditer()accept anAbortSignalfor clean teardown. - Cross-runtime — identical API across Deno, Node.js, Bun, and all modern browsers.
// Deno / JSR
import { MightyEmitter } from "@wxt/mightyemitter";
// Node / Bun (after installing from JSR)
// npx jsr add @wxt/mightyemitter
import { MightyEmitter } from "@wxt/mightyemitter";import { MightyEmitter } from "@wxt/mightyemitter";
// 1. Define your event map
type Events = {
message: string;
error: Error;
close: void;
};
// 2. Create an emitter
const emitter = new MightyEmitter<Events>();
// 3. Subscribe
const off = emitter.on("message", (msg) => {
console.log("Received:", msg);
});
// 4. Emit
emitter.emit("message", "hello"); // Received: hello
// 5. Unsubscribe when done
off();Subscribe to an event. Returns an unsubscribe function.
const off = emitter.on("message", (msg) => console.log(msg));
off(); // stop listeningSubscribe to an event for a single firing, then auto-unsubscribe.
emitter.once("message", (msg) => console.log("Only once:", msg));Remove a specific listener by reference.
const handler = (msg: string) => console.log(msg);
emitter.on("message", handler);
emitter.off("message", handler);Emit an event synchronously. Returns true if the event had listeners.
For void events, data is omitted.
emitter.emit("message", "hello"); // true
emitter.emit("close"); // void event, no payload
emitter.emit("message", "nobody"); // false if no listenersReturns a promise that resolves the next time the event fires. Supports AbortSignal for cancellation.
const msg = await emitter.next("message");
// With timeout:
const msg = await emitter.next("message", {
signal: AbortSignal.timeout(5000),
});Returns an async iterator that yields each time the event fires. Supports AbortSignal to stop iteration.
const ac = new AbortController();
for await (const msg of emitter.iter("message", { signal: ac.signal })) {
console.log(msg);
if (msg === "done") ac.abort();
}Returns the number of listeners for a given event, or the total across all events if no event is specified.
emitter.listenerCount("message"); // 2
emitter.listenerCount(); // total across all eventsRemove all listeners for a given event, or all listeners entirely.
emitter.clear("message"); // clear only "message" listeners
emitter.clear(); // clear everythingTypeScript enforces correct payload types at compile time:
type Events = {
data: { id: number; value: string };
done: void;
};
const ee = new MightyEmitter<Events>();
ee.on("data", (payload) => {
// payload is { id: number; value: string } — fully typed
console.log(payload.id, payload.value);
});
ee.emit("data", { id: 1, value: "hello" });
ee.emit("done"); // no payload neededtype SocketEvents = {
open: void;
message: string;
close: { code: number; reason: string };
};
class Socket extends MightyEmitter<SocketEvents> {
connect() {
// ...
this.emit("open");
}
send(data: string) {
// ...
}
}
const socket = new Socket();
socket.on("message", (msg) => console.log(msg));
socket.connect();async function waitForReady(emitter: MightyEmitter<{ ready: void }>) {
await emitter.next("ready");
console.log("System is ready");
}async function processStream(emitter: MightyEmitter<{ data: number }>) {
const ac = new AbortController();
for await (const value of emitter.iter("data", { signal: ac.signal })) {
console.log(value);
if (value < 0) ac.abort(); // stop on negative
}
}Run the included benchmarks with:
deno benchMightyEmitter uses a Map<K, Set<Listener>> internally for O(1) add/delete and
safe iteration, keeping emit-per-listener overhead minimal even at scale.
| Runtime | Supported |
|---|---|
| Deno | Yes |
| Node.js (via JSR) | Yes |
| Bun | Yes |
| Modern browsers | Yes |
Contributions, issues, and feature requests are welcome!
- Fork the repo
- Create your feature branch (
git checkout -b feat/amazing-feature) - Run
deno task testanddeno task check - Open a pull request
MIT — free for personal and commercial use.
## Development
Requires [Deno](https://deno.land/).
```sh
# Type-check
deno task check
# Run tests
deno task test
# Run benchmarks
deno task bench
MightyEmitter is a single-class, single-file module (~230 LoC) with no dependencies and no build step. The entire public API is one class and three type aliases.
Internal data structure: Listeners are stored in a Map<event, Set<listener>>.
This gives O(1) subscribe, O(1) unsubscribe, and O(n) emit where n is only the
listeners for that specific event — other events are untouched.
-
Type safety at compile time. The
EventMapgeneric enforces that everyemit,on,once, andnextcall uses the correct event name and payload type. Typos and wrong types are caught before code ever runs. -
Snapshot iteration.
emitspreads the listenerSetinto an array before iterating ([...set]), then checksset.has(listener)before each call. This means listeners added during an emit cycle do not fire in that cycle, and listeners removed mid-cycle are correctly skipped. No stale references, no infinite loops. -
Idempotent unsubscribe. The
Unsubscribefunction returned byonuses aremovedflag so calling it multiple times is a no-op — no risk of accidentally removing a different listener. -
Automatic cleanup. When the last listener for an event is removed, the event key is deleted from the
Map, preventing unbounded memory growth from events that are no longer in use. -
AbortSignal support. Both
nextanditeraccept anAbortSignal, giving clean cancellation semantics. Abort listeners are properly removed on resolve/reject to avoid memory leaks. -
Private internals. The listener
Mapis a#privatefield — external code cannot tamper with or iterate over registered listeners.
-
Zero dependencies. No supply-chain surface. No transitive packages to audit or worry about. The entire attack surface is one file you can read in five minutes.
-
No dynamic code execution. No
eval, noFunction(), no string-based event dispatch tricks. Event names are statically typed string literals. -
No global state. Each
MightyEmitterinstance is fully isolated. There are no shared registries, singletons, or ambient side effects. -
Strict compiler settings. The project builds with
strict: trueandnoUncheckedIndexedAccess: true, catching null/undefined access and implicitanyat compile time.
-
Map+Setis the optimal JS data structure for this pattern. Subscribe and unsubscribe are O(1). Emit iterates only the listeners for the targeted event. -
No wrapper overhead. There are no middleware chains, priority queues, wildcard matchers, or regex-based event routing. The hot path through
emitis a singleSetspread +forloop. -
No allocations on unsubscribe. The
off/unsubscribe path deletes from theSetin-place. When theSetdrains to zero, theMapkey is removed. -
Benchmarked. The project includes
Deno.benchtests covering emit fan-out (1, 10, and 100 listeners), subscribe/unsubscribe churn, void events, andoncelifecycle — so regressions are measurable.
The test suite covers:
| Area | Cases |
|---|---|
on / emit |
basic delivery, multiple listeners, subscription order, return value, void events |
off / unsubscribe |
removal by reference, double-call safety, empty set cleanup, no-op on unknown event |
once |
single-fire guarantee, pre-fire unsubscribe |
listenerCount |
per-event, total, after removal |
clear |
per-event, global |
next |
promise resolution, pre-aborted signal, mid-wait abort, abort listener cleanup |
iter |
async iteration with abort |
| Edge cases | add-during-emit, remove-during-emit, error propagation, duplicate reference dedup, event isolation |
MIT