Repository: sindresorhus/emittery (js)

Issue:
Support Symbol as `eventName`
<!-- Issuehunt Badges -->
[<img alt="Issuehunt badges" src="https://img.shields.io/badge/IssueHunt-%2460%20Funded-%2300A156.svg" />](https://issuehunt.io/r/sindresorhus/emittery/issues/16)
<!-- /Issuehunt Badges -->


See https://github.com/sindresorhus/emittery/issues/12#issuecomment-348813948 for use-case.

I think #13 should be implemented first though, so we can make sure it's not hard to define in a TS type definition.

---

> *Note: This issue has a bounty, so it's expected that you are an experienced programmer and that you give it your best effort if you intend to tackle this. Don't forget, if applicable, to add tests, docs (double-check for typos), and update TypeScript definitions. And don't be sloppy. Review your own diff multiple times and try to find ways to improve and simplify your code. Instead of asking too many questions, present solutions. The point of an issue bounty is to reduce my workload, not give me more. Include a 🦄 in your PR description to indicate that you've read this. Thanks for helping out 🙌* - @sindresorhus


<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>


### Backers (Total: $60.00)

- [<img src='https://avatars3.githubusercontent.com/u/44827199?v=4' alt='issuehunt' width=24 height=24> issuehunt](https://issuehunt.io/u/issuehunt) ($60.00)
### Submitted pull Requests
- [#43 Allow symbol event names](https://issuehunt.io/r/sindresorhus/emittery/pull/43)
---

#### [Become a backer now!](https://issuehunt.io/r/sindresorhus/emittery/issues/16)
#### [Or submit a pull request to get the deposits!](https://issuehunt.io/r/sindresorhus/emittery/issues/16)
### Tips

- Checkout the [Issuehunt explorer](https://issuehunt.io/r/sindresorhus/emittery/) to discover more funded issues.
- Need some help from other developers? [Add your repositories](https://issuehunt.io/r/new) on IssueHunt to raise funds.
---
IssueHunt has been backed by the following sponsors. [Become a sponsor](https://issuehunt.io/membership/members)
</details>
<!-- /Issuehunt content-->

Pull request description:
Allow symbol event names
Fixes #16 

In typescript, complete typing is blocked by https://github.com/microsoft/TypeScript/issues/1863 at the moment, so we relax typing a bit. This relaxation affects defining `EventDataMap` with number indexes, so `Emittery.Typed<{123: string}, 'someevent'>` causes no compile time error. However, *using* any such event will still cause compile time error.

:unicorn: 
<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>

### Referenced issues

This pull request has been submitted to:
- [#16: Support Symbol as `eventName`](https://issuehunt.io/repos/112248999/issues/16)
---

IssueHunt has been backed by the following sponsors. [Become a sponsor](https://issuehunt.io/membership/members)
</details>
<!-- /Issuehunt content-->

Patch:
diff --git a/index.d.ts b/index.d.ts
index 7d0491c..1d1213a 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,3 +1,5 @@
+type EventName = string | symbol;
+
 declare class Emittery {
 	/**
 	In TypeScript, it returns a decorator which mixins `Emittery` as property `emitteryPropertyName` and `methodNames`, or all `Emittery` methods if `methodNames` is not defined, into the target class.
@@ -23,7 +25,7 @@ declare class Emittery {
 
 	@returns An unsubscribe method.
 	*/
-	on(eventName: string, listener: (eventData?: unknown) => void): Emittery.UnsubscribeFn;
+	on(eventName: EventName, listener: (eventData?: unknown) => void): Emittery.UnsubscribeFn;
 
 	/**
 	Get an async iterator which buffers data each time an event is emitted.
@@ -78,12 +80,12 @@ declare class Emittery {
 	}
 	```
 	*/
-	events(eventName:string): AsyncIterableIterator<unknown>
+	events(eventName:EventName): AsyncIterableIterator<unknown>
 
 	/**
 	Remove an event subscription.
 	*/
-	off(eventName: string, listener: (eventData?: unknown) => void): void;
+	off(eventName: EventName, listener: (eventData?: unknown) => void): void;
 
 	/**
 	Subscribe to an event only once. It will be unsubscribed after the first
@@ -91,14 +93,14 @@ declare class Emittery {
 
 	@returns The event data when `eventName` is emitted.
 	*/
-	once(eventName: string): Promise<unknown>;
+	once(eventName: EventName): Promise<unknown>;
 
 	/**
 	Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but executed concurrently.
 
 	@returns A promise that resolves when all the event listeners are done. *Done* meaning executed if synchronous or resolved when an async/promise-returning function. You usually wouldn't want to wait for this, but you could for example catch possible errors. If any of the listeners throw/reject, the returned promise will be rejected with the error, but the other listeners will not be affected.
 	*/
-	emit(eventName: string, eventData?: unknown): Promise<void>;
+	emit(eventName: EventName, eventData?: unknown): Promise<void>;
 
 	/**
 	Same as `emit()`, but it waits for each listener to resolve before triggering the next one. This can be useful if your events depend on each other. Although ideally they should not. Prefer `emit()` whenever possible.
@@ -107,14 +109,14 @@ declare class Emittery {
 
 	@returns A promise that resolves when all the event listeners are done.
 	*/
-	emitSerial(eventName: string, eventData?: unknown): Promise<void>;
+	emitSerial(eventName: EventName, eventData?: unknown): Promise<void>;
 
 	/**
 	Subscribe to be notified about any event.
 
 	@returns A method to unsubscribe.
 	*/
-	onAny(listener: (eventName: string, eventData?: unknown) => unknown): Emittery.UnsubscribeFn;
+	onAny(listener: (eventName: EventName, eventData?: unknown) => unknown): Emittery.UnsubscribeFn;
 
 	/**
 	Get an async iterator which buffers a tuple of an event name and data each time an event is emitted.
@@ -155,19 +157,19 @@ declare class Emittery {
 	/**
 	Remove an `onAny` subscription.
 	*/
-	offAny(listener: (eventName: string, eventData?: unknown) => void): void;
+	offAny(listener: (eventName: EventName, eventData?: unknown) => void): void;
 
 	/**
 	Clear all event listeners on the instance.
 
 	If `eventName` is given, only the listeners for that event are cleared.
 	*/
-	clearListeners(eventName?: string): void;
+	clearListeners(eventName?: EventName): void;
 
 	/**
 	The number of listeners for the `eventName` or all events if not specified.
 	*/
-	listenerCount(eventName?: string): number;
+	listenerCount(eventName?: EventName): number;
 
 	/**
 	Bind the given `methodNames`, or all `Emittery` methods if `methodNames` is not defined, into the `target` object.
@@ -191,12 +193,14 @@ declare namespace Emittery {
 	Removes an event subscription.
 	*/
 	type UnsubscribeFn = () => void;
+	type EventNameFromDataMap<EventDataMap> = Extract<keyof EventDataMap, EventName>;
 
 	/**
 	Maps event names to their emitted data type.
 	*/
 	interface Events {
-		[eventName: string]: any;
+		// Blocked by https://github.com/microsoft/TypeScript/issues/1863, should be
+		// `[eventName: EventName]: unknown;`
 	}
 
 	/**
@@ -216,27 +220,27 @@ declare namespace Emittery {
 	emitter.emit('end'); // TS compilation error
 	```
 	*/
-	class Typed<EventDataMap extends Events, EmptyEvents extends string = never> extends Emittery {
-		on<Name extends Extract<keyof EventDataMap, string>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): Emittery.UnsubscribeFn;
+	class Typed<EventDataMap extends Events, EmptyEvents extends EventName = never> extends Emittery {
+		on<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): Emittery.UnsubscribeFn;
 		on<Name extends EmptyEvents>(eventName: Name, listener: () => void): Emittery.UnsubscribeFn;
 
-		events<Name extends Extract<keyof EventDataMap, string>>(eventName: Name): AsyncIterableIterator<EventDataMap[Name]>;
+		events<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name): AsyncIterableIterator<EventDataMap[Name]>;
 
-		once<Name extends Extract<keyof EventDataMap, string>>(eventName: Name): Promise<EventDataMap[Name]>;
+		once<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name): Promise<EventDataMap[Name]>;
 		once<Name extends EmptyEvents>(eventName: Name): Promise<void>;
 
-		off<Name extends Extract<keyof EventDataMap, string>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): void;
+		off<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): void;
 		off<Name extends EmptyEvents>(eventName: Name, listener: () => void): void;
 
-		onAny(listener: (eventName: Extract<keyof EventDataMap, string> | EmptyEvents, eventData?: EventDataMap[Extract<keyof EventDataMap, string>]) => void): Emittery.UnsubscribeFn;
-		anyEvent(): AsyncIterableIterator<[Extract<keyof EventDataMap, string>, EventDataMap[Extract<keyof EventDataMap, string>]]>;
+		onAny(listener: (eventName: EventNameFromDataMap<EventDataMap> | EmptyEvents, eventData?: EventDataMap[EventNameFromDataMap<EventDataMap>]) => void): Emittery.UnsubscribeFn;
+		anyEvent(): AsyncIterableIterator<[EventNameFromDataMap<EventDataMap>, EventDataMap[EventNameFromDataMap<EventDataMap>]]>;
 
-		offAny(listener: (eventName: Extract<keyof EventDataMap, string> | EmptyEvents, eventData?: EventDataMap[Extract<keyof EventDataMap, string>]) => void): void;
+		offAny(listener: (eventName: EventNameFromDataMap<EventDataMap> | EmptyEvents, eventData?: EventDataMap[EventNameFromDataMap<EventDataMap>]) => void): void;
 
-		emit<Name extends Extract<keyof EventDataMap, string>>(eventName: Name, eventData: EventDataMap[Name]): Promise<void>;
+		emit<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, eventData: EventDataMap[Name]): Promise<void>;
 		emit<Name extends EmptyEvents>(eventName: Name): Promise<void>;
 
-		emitSerial<Name extends Extract<keyof EventDataMap, string>>(eventName: Name, eventData: EventDataMap[Name]): Promise<void>;
+		emitSerial<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, eventData: EventDataMap[Name]): Promise<void>;
 		emitSerial<Name extends EmptyEvents>(eventName: Name): Promise<void>;
 	}
 }
diff --git a/index.js b/index.js
index 0a75dc2..cd5b0fc 100644
--- a/index.js
+++ b/index.js
@@ -7,8 +7,8 @@ const anyProducer = Symbol('anyProducer');
 const resolvedPromise = Promise.resolve();
 
 function assertEventName(eventName) {
-	if (typeof eventName !== 'string') {
-		throw new TypeError('eventName must be a string');
+	if (typeof eventName !== 'string' && typeof eventName !== 'symbol') {
+		throw new TypeError('eventName must be a string or a symbol');
 	}
 }
 
diff --git a/readme.md b/readme.md
index d2f7a2f..b466e5f 100644
--- a/readme.md
+++ b/readme.md
@@ -25,15 +25,27 @@ const emitter = new Emittery();
 
 emitter.on('🦄', data => {
 	console.log(data);
-	// '🌈'
 });
 
-emitter.emit('🦄', '🌈');
+const myUnicorn = Symbol('🦄');
+
+emitter.on(myUnicorn, data => {
+	console.log(`Unicorns love ${data}`);
+});
+
+emitter.emit('🦄', '🌈'); // Will trigger printing '🌈'
+emitter.emit(myUnicorn, '🦋');  // Will trigger printing 'Unicorns love 🦋'
+
 ```
 
 
 ## API
 
+### eventName
+
+Emittery accepts strings and symbols as event names.
+Symbol event names can be used to avoid name collisions when your classes are extended, especially for internal events.
+
 ### emitter = new Emittery()
 
 #### on(eventName, listener)


Test patch:
diff --git a/test/index.js b/test/index.js
index 5889c5e..24bd9d2 100644
--- a/test/index.js
+++ b/test/index.js
@@ -15,8 +15,23 @@ test('on()', async t => {
 	t.deepEqual(calls, [1, 2]);
 });
 
-test('on() - eventName must be a string', t => {
+test('on() - symbol eventName', async t => {
 	const emitter = new Emittery();
+	const eventName = Symbol('eventName');
+	const calls = [];
+	const listener1 = () => calls.push(1);
+	const listener2 = () => calls.push(2);
+	emitter.on(eventName, listener1);
+	emitter.on(eventName, listener2);
+	await emitter.emit(eventName);
+	t.deepEqual(calls, [1, 2]);
+});
+
+test('on() - eventName must be a string or a symbol', t => {
+	const emitter = new Emittery();
+
+	emitter.on('string', () => {});
+	emitter.on(Symbol('symbol'), () => {});
 
 	t.throws(() => {
 		emitter.on(42, () => {});
@@ -137,9 +152,12 @@ test('off()', async t => {
 	t.deepEqual(calls, [1]);
 });
 
-test('off() - eventName must be a string', t => {
+test('off() - eventName must be a string or a symbol', t => {
 	const emitter = new Emittery();
 
+	emitter.on('string', () => {});
+	emitter.on(Symbol('symbol'), () => {});
+
 	t.throws(() => {
 		emitter.off(42);
 	}, TypeError);
@@ -161,8 +179,12 @@ test('once()', async t => {
 	t.is(await promise, fixture);
 });
 
-test('once() - eventName must be a string', async t => {
+test('once() - eventName must be a string or a symbol', async t => {
 	const emitter = new Emittery();
+
+	emitter.once('string');
+	emitter.once(Symbol('symbol'));
+
 	await t.throwsAsync(emitter.once(42), TypeError);
 });
 
@@ -202,8 +224,12 @@ test.cb('emit() - multiple events', t => {
 	emitter.emit('🦄');
 });
 
-test('emit() - eventName must be a string', async t => {
+test('emit() - eventName must be a string or a symbol', async t => {
 	const emitter = new Emittery();
+
+	emitter.emit('string');
+	emitter.emit(Symbol('symbol'));
+
 	await t.throwsAsync(emitter.emit(42), TypeError);
 });
 
@@ -302,8 +328,12 @@ test.cb('emitSerial()', t => {
 	emitter.emitSerial('🦄', 'e');
 });
 
-test('emitSerial() - eventName must be a string', async t => {
+test('emitSerial() - eventName must be a string or a symbol', async t => {
 	const emitter = new Emittery();
+
+	emitter.emitSerial('string');
+	emitter.emitSerial(Symbol('symbol'));
+
 	await t.throwsAsync(emitter.emitSerial(42), TypeError);
 });
 
@@ -562,9 +592,13 @@ test('listenerCount() - works with empty eventName strings', t => {
 	t.is(emitter.listenerCount(''), 1);
 });
 
-test('listenerCount() - eventName must be undefined if not a string', t => {
+test('listenerCount() - eventName must be undefined if not a string nor a symbol', t => {
 	const emitter = new Emittery();
 
+	emitter.listenerCount('string');
+	emitter.listenerCount(Symbol('symbol'));
+	emitter.listenerCount();
+
 	t.throws(() => {
 		emitter.listenerCount(42);
 	}, TypeError);