Repository: sindresorhus/emittery
Pull request #84 (2021-04-30 17:57:13)

Fix support for typed subscriptions for multiple event names for `.on()`
Closes #82

sindresorhus: Can you add it to all the methods that accept it? See: https://github.com/sindresorhus/emittery/pull/82/files
arthur-er: > Can you add it to all the methods that accept it? See: https://github.com/sindresorhus/emittery/pull/82/files

I'm unsure about how to add the tests. But I added the type into the other methods
sindresorhus: They need to be tested for this to be merged.
arthur-er: > They need to be tested for this to be merged.

Tests added!
sindresorhus: Tests are failing. Please always run `nom test` locally before pushing changes.
arthur-er: I'm getting issues with pEvent compatibility, the emitter in pEvent don't expect the .on() method to receive an array

Patch:
diff --git a/.gitattributes b/.gitattributes
index ed46090..6313b56 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,1 @@
 * text=auto eol=lf
-*.psd binary
diff --git a/.github/funding.yml b/.github/funding.yml
deleted file mode 100644
index 0038dab..0000000
--- a/.github/funding.yml
+++ /dev/null
@@ -1,1 +0,0 @@
-github: sindresorhus
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c546fbd..b207add 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,12 +10,12 @@ jobs:
       fail-fast: false
       matrix:
         node-version:
+          - 16
           - 14
           - 12
-          - 10
     steps:
       - uses: actions/checkout@v2
-      - uses: actions/setup-node@v1
+      - uses: actions/setup-node@v2
         with:
           node-version: ${{ matrix.node-version }}
       - run: npm install
diff --git a/examples/clock.js b/examples/clock.js
index 79d1cc3..54c56d8 100755
--- a/examples/clock.js
+++ b/examples/clock.js
@@ -53,9 +53,9 @@ function onTick({duration}) {
 	}
 }
 
-function onError(err) {
+function onError(error) {
 	process.exitCode = 1;
-	console.error(err);
+	console.error(error);
 	stop();
 }
 
diff --git a/examples/clocktyped.ts b/examples/clocktyped.ts
index a6715b2..c2d2979 100755
--- a/examples/clocktyped.ts
+++ b/examples/clocktyped.ts
@@ -66,9 +66,9 @@ function onTick({duration}: TickData) {
 	}
 }
 
-function onError(err: Error) {
+function onError(error: Error) {
 	process.exitCode = 1;
-	console.error(err);
+	console.error(error);
 	stop();
 }
 
diff --git a/index.d.ts b/index.d.ts
index 37da6a1..dd40236 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,3 +1,5 @@
+/* eslint-disable no-redeclare */
+
 /**
 Emittery accepts strings and symbols as event names.
 
@@ -14,6 +16,122 @@ declare const listenerAdded: unique symbol;
 declare const listenerRemoved: unique symbol;
 type OmnipresentEventData = {[listenerAdded]: Emittery.ListenerChangedData; [listenerRemoved]: Emittery.ListenerChangedData};
 
+/**
+Emittery can collect and log debug information.
+
+To enable this feature set the `DEBUG` environment variable to `emittery` or `*`. Additionally, you can set the static `isDebugEnabled` variable to true on the Emittery class, or `myEmitter.debug.enabled` on an instance of it for debugging a single instance.
+
+See API for more information on how debugging works.
+*/
+type DebugLogger<EventData, Name extends keyof EventData> = (type: string, debugName: string, eventName?: Name, eventData?: EventData[Name]) => void;
+
+/**
+Configure debug options of an instance.
+*/
+interface DebugOptions<EventData> {
+	/**
+	Define a name for the instance of Emittery to use when outputting debug data.
+
+	@default undefined
+
+	@example
+	```
+	import Emittery = require('emittery');
+
+	Emittery.isDebugEnabled = true;
+
+	const emitter = new Emittery({debug: {name: 'myEmitter'}});
+
+	emitter.on('test', data => {
+		// …
+	});
+
+	emitter.emit('test');
+	//=> [16:43:20.417][emittery:subscribe][myEmitter] Event Name: test
+	//	data: undefined
+	```
+	*/
+	readonly name: string;
+
+	/**
+	Toggle debug logging just for this instance.
+
+	@default false
+
+	@example
+	```
+	import Emittery = require('emittery');
+
+	const emitter1 = new Emittery({debug: {name: 'emitter1', enabled: true}});
+	const emitter2 = new Emittery({debug: {name: 'emitter2'}});
+
+	emitter1.on('test', data => {
+		// …
+	});
+
+	emitter2.on('test', data => {
+		// …
+	});
+
+	emitter1.emit('test');
+	//=> [16:43:20.417][emittery:subscribe][emitter1] Event Name: test
+	//	data: undefined
+
+	emitter2.emit('test');
+	```
+	*/
+	enabled?: boolean;
+
+	/**
+	Function that handles debug data.
+
+	@default
+	```
+	(type, debugName, eventName, eventData) => {
+		eventData = JSON.stringify(eventData);
+
+		if (typeof eventName === 'symbol') {
+			eventName = eventName.toString();
+		}
+
+		const currentTime = new Date();
+		const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;
+		console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`);
+	}
+	```
+
+	@example
+	```
+	import Emittery = require('emittery');
+
+	const myLogger = (type, debugName, eventName, eventData) => console.log(`[${type}]: ${eventName}`);
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'myEmitter',
+			enabled: true,
+			logger: myLogger
+		}
+	});
+
+	emitter.on('test', data => {
+		// …
+	});
+
+	emitter.emit('test');
+	//=> [subscribe]: test
+	```
+	*/
+	logger?: DebugLogger<EventData, keyof EventData>;
+}
+
+/**
+Configuration options for Emittery.
+*/
+interface Options<EventData> {
+	debug?: DebugOptions<EventData>;
+}
+
 /**
 Emittery is a strictly typed, fully async EventEmitter implementation. Event listeners can be registered with `on` or `once`, and events can be emitted with `emit`.
 
@@ -50,6 +168,39 @@ declare class Emittery<
 	AllEventData = EventData & OmnipresentEventData,
 	DatalessEvents = DatalessEventNames<EventData>
 > {
+	/**
+	Toggle debug mode for all instances.
+
+	Default: `true` if the `DEBUG` environment variable is set to `emittery` or `*`, otherwise `false`.
+
+	@example
+	```
+	import Emittery = require('emittery');
+
+	Emittery.isDebugEnabled = true;
+
+	const emitter1 = new Emittery({debug: {name: 'myEmitter1'}});
+	const emitter2 = new Emittery({debug: {name: 'myEmitter2'}});
+
+	emitter1.on('test', data => {
+		// …
+	});
+
+	emitter2.on('otherTest', data => {
+		// …
+	});
+
+	emitter1.emit('test');
+	//=> [16:43:20.417][emittery:subscribe][myEmitter1] Event Name: test
+	//	data: undefined
+
+	emitter2.emit('otherTest');
+	//=> [16:43:20.417][emittery:subscribe][myEmitter2] Event Name: otherTest
+	//	data: undefined
+	```
+	*/
+	static isDebugEnabled: boolean;
+
 	/**
 	Fires when an event listener was added.
 
@@ -104,6 +255,18 @@ declare class Emittery<
 	*/
 	static readonly listenerRemoved: typeof listenerRemoved;
 
+	/**
+	Debugging options for the current instance.
+	*/
+	debug: DebugOptions<EventData>;
+
+	/**
+	Create a new Emittery instance with the specified options.
+
+	@returns An instance of Emittery that you can use to listen for and emit events.
+	*/
+	constructor(options?: Options<EventData>);
+
 	/**
 	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.
 
@@ -122,7 +285,7 @@ declare class Emittery<
 	static mixin(
 		emitteryPropertyName: string | symbol,
 		methodNames?: readonly string[]
-	): <T extends { new (): any }>(klass: T) => T; // eslint-disable-line @typescript-eslint/prefer-function-type
+	): <T extends {new (): any}>(klass: T) => T; // eslint-disable-line @typescript-eslint/prefer-function-type
 
 	/**
 	Subscribe to one or more events.
@@ -140,6 +303,7 @@ declare class Emittery<
 	emitter.on('🦄', data => {
 		console.log(data);
 	});
+
 	emitter.on(['🦄', '🐶'], data => {
 		console.log(data);
 	});
@@ -149,7 +313,7 @@ declare class Emittery<
 	```
 	*/
 	on<Name extends keyof AllEventData>(
-		eventName: Name,
+		eventName: Name | Name[],
 		listener: (eventData: AllEventData[Name]) => void | Promise<void>
 	): Emittery.UnsubscribeFn;
 
@@ -264,7 +428,7 @@ declare class Emittery<
 	```
 	*/
 	off<Name extends keyof AllEventData>(
-		eventName: Name,
+		eventName: Name | Name[],
 		listener: (eventData: AllEventData[Name]) => void | Promise<void>
 	): void;
 
@@ -284,6 +448,7 @@ declare class Emittery<
 		console.log(data);
 		//=> '🌈'
 	});
+
 	emitter.once(['🦄', '🐶']).then(data => {
 		console.log(data);
 	});
@@ -292,7 +457,7 @@ declare class Emittery<
 	emitter.emit('🐶', '🍖'); // Nothing happens
 	```
 	*/
-	once<Name extends keyof AllEventData>(eventName: Name): Promise<AllEventData[Name]>;
+	once<Name extends keyof AllEventData>(eventName: Name | Name[]): Promise<AllEventData[Name]>;
 
 	/**
 	Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but executed concurrently.
@@ -383,12 +548,12 @@ declare class Emittery<
 
 	If `eventName` is given, only the listeners for that event are cleared.
 	*/
-	clearListeners(eventName?: keyof EventData): void;
+	clearListeners<Name extends keyof EventData>(eventName?: Name | Name[]): void;
 
 	/**
 	The number of listeners for the `eventName` or all events if not specified.
 	*/
-	listenerCount(eventName?: keyof EventData): number;
+	listenerCount<Name extends keyof EventData>(eventName?: Name | Name[]): number;
 
 	/**
 	Bind the given `methodNames`, or all `Emittery` methods if `methodNames` is not defined, into the `target` object.
diff --git a/index.js b/index.js
index 293b664..cd4a5ae 100644
--- a/index.js
+++ b/index.js
@@ -9,6 +9,8 @@ const resolvedPromise = Promise.resolve();
 const listenerAdded = Symbol('listenerAdded');
 const listenerRemoved = Symbol('listenerRemoved');
 
+let isGlobalDebugEnabled = false;
+
 function assertEventName(eventName) {
 	if (typeof eventName !== 'string' && typeof eventName !== 'symbol') {
 		throw new TypeError('eventName must be a string or a symbol');
@@ -189,10 +191,48 @@ class Emittery {
 		};
 	}
 
-	constructor() {
+	static get isDebugEnabled() {
+		if (typeof process !== 'object') {
+			return isGlobalDebugEnabled;
+		}
+
+		const {env} = process || {env: {}};
+		return env.DEBUG === 'emittery' || env.DEBUG === '*' || isGlobalDebugEnabled;
+	}
+
+	static set isDebugEnabled(newValue) {
+		isGlobalDebugEnabled = newValue;
+	}
+
+	constructor(options = {}) {
 		anyMap.set(this, new Set());
 		eventsMap.set(this, new Map());
 		producersMap.set(this, new Map());
+		this.debug = options.debug || {};
+
+		if (this.debug.enabled === undefined) {
+			this.debug.enabled = false;
+		}
+
+		if (!this.debug.logger) {
+			this.debug.logger = (type, debugName, eventName, eventData) => {
+				eventData = JSON.stringify(eventData);
+
+				if (typeof eventName === 'symbol') {
+					eventName = eventName.toString();
+				}
+
+				const currentTime = new Date();
+				const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;
+				console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`);
+			};
+		}
+	}
+
+	logIfDebugEnabled(type, eventName, eventData) {
+		if (Emittery.isDebugEnabled || this.debug.enabled) {
+			this.debug.logger(type, this.debug.name, eventName, eventData);
+		}
 	}
 
 	on(eventNames, listener) {
@@ -203,6 +243,8 @@ class Emittery {
 			assertEventName(eventName);
 			getListeners(this, eventName).add(listener);
 
+			this.logIfDebugEnabled('subscribe', eventName, undefined);
+
 			if (!isListenerSymbol(eventName)) {
 				this.emit(listenerAdded, {eventName, listener});
 			}
@@ -219,6 +261,8 @@ class Emittery {
 			assertEventName(eventName);
 			getListeners(this, eventName).delete(listener);
 
+			this.logIfDebugEnabled('unsubscribe', eventName, undefined);
+
 			if (!isListenerSymbol(eventName)) {
 				this.emit(listenerRemoved, {eventName, listener});
 			}
@@ -246,6 +290,8 @@ class Emittery {
 	async emit(eventName, eventData) {
 		assertEventName(eventName);
 
+		this.logIfDebugEnabled('emit', eventName, eventData);
+
 		enqueueProducers(this, eventName, eventData);
 
 		const listeners = getListeners(this, eventName);
@@ -271,6 +317,8 @@ class Emittery {
 	async emitSerial(eventName, eventData) {
 		assertEventName(eventName);
 
+		this.logIfDebugEnabled('emitSerial', eventName, eventData);
+
 		const listeners = getListeners(this, eventName);
 		const anyListeners = anyMap.get(this);
 		const staticListeners = [...listeners];
@@ -294,6 +342,9 @@ class Emittery {
 
 	onAny(listener) {
 		assertListener(listener);
+
+		this.logIfDebugEnabled('subscribeAny', undefined, undefined);
+
 		anyMap.get(this).add(listener);
 		this.emit(listenerAdded, {listener});
 		return this.offAny.bind(this, listener);
@@ -305,6 +356,9 @@ class Emittery {
 
 	offAny(listener) {
 		assertListener(listener);
+
+		this.logIfDebugEnabled('unsubscribeAny', undefined, undefined);
+
 		this.emit(listenerRemoved, {listener});
 		anyMap.get(this).delete(listener);
 	}
@@ -313,6 +367,8 @@ class Emittery {
 		eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
 
 		for (const eventName of eventNames) {
+			this.logIfDebugEnabled('clear', eventName, undefined);
+
 			if (typeof eventName === 'string' || typeof eventName === 'symbol') {
 				getListeners(this, eventName).clear();
 
diff --git a/package.json b/package.json
index 2390cba..4ec9a5c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "emittery",
-	"version": "0.8.1",
+	"version": "0.9.2",
 	"description": "Simple and modern async event emitter",
 	"license": "MIT",
 	"repository": "sindresorhus/emittery",
@@ -11,7 +11,7 @@
 		"url": "https://sindresorhus.com"
 	},
 	"engines": {
-		"node": ">=10"
+		"node": ">=12"
 	},
 	"scripts": {
 		"test": "xo && nyc ava && tsd"
@@ -48,13 +48,13 @@
 		"typed"
 	],
 	"devDependencies": {
-		"@types/node": "^13.7.5",
+		"@types/node": "^15.6.1",
 		"ava": "^2.4.0",
 		"delay": "^4.3.0",
 		"nyc": "^15.0.0",
 		"p-event": "^4.1.0",
-		"tsd": "^0.14.0",
-		"xo": "^0.36.1"
+		"tsd": "^0.16.0",
+		"xo": "^0.39.0"
 	},
 	"nyc": {
 		"reporter": [
diff --git a/readme.md b/readme.md
index 2ae169a..0469b80 100644
--- a/readme.md
+++ b/readme.md
@@ -44,7 +44,156 @@ 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()
+### isDebugEnabled
+
+Toggle debug mode for all instances.
+
+Default: `true` if the `DEBUG` environment variable is set to `emittery` or `*`, otherwise `false`.
+
+Example:
+
+```js
+const Emittery = require('emittery');
+
+Emittery.isDebugEnabled = true;
+
+const emitter1 = new Emittery({debug: {name: 'myEmitter1'}});
+const emitter2 = new Emittery({debug: {name: 'myEmitter2'}});
+
+emitter1.on('test', data => {
+	// …
+});
+
+emitter2.on('otherTest', data => {
+	// …
+});
+
+emitter1.emit('test');
+//=> [16:43:20.417][emittery:subscribe][myEmitter1] Event Name: test
+//	data: undefined
+
+emitter2.emit('otherTest');
+//=> [16:43:20.417][emittery:subscribe][myEmitter2] Event Name: otherTest
+//	data: undefined
+```
+
+### emitter = new Emittery(options?)
+
+Create a new instance of Emittery.
+
+#### options?
+
+Type: `object`
+
+Configure the new instance of Emittery.
+
+##### debug?
+
+Type: `objcect`
+
+Configure the debugging options for this instance.
+
+###### name
+
+Type: `string`\
+Default: `undefined`
+
+Define a name for the instance of Emittery to use when outputting debug data.
+
+Example:
+
+```js
+const Emittery = require('emittery');
+
+Emittery.isDebugEnabled = true;
+
+const emitter = new Emittery({debug: {name: 'myEmitter'}});
+
+emitter.on('test', data => {
+	// …
+});
+
+emitter.emit('test');
+//=> [16:43:20.417][emittery:subscribe][myEmitter] Event Name: test
+//	data: undefined
+```
+
+###### enabled?
+
+Type: `boolean`\
+Default: `false`
+
+Toggle debug logging just for this instance.
+
+Example:
+
+```js
+const Emittery = require('emittery');
+
+const emitter1 = new Emittery({debug: {name: 'emitter1', enabled: true}});
+const emitter2 = new Emittery({debug: {name: 'emitter2'}});
+
+emitter1.on('test', data => {
+	// …
+});
+
+emitter2.on('test', data => {
+	// …
+});
+
+emitter1.emit('test');
+//=> [16:43:20.417][emittery:subscribe][emitter1] Event Name: test
+//	data: undefined
+
+emitter2.emit('test');
+```
+
+###### logger?
+
+Type: `Function(string, string, EventName?, Record<string, any>?) => void`
+
+Default:
+
+```js
+(type, debugName, eventName, eventData) => {
+	if (typeof eventData === 'object') {
+		eventData = JSON.stringify(eventData);
+	}
+
+	if (typeof eventName === 'symbol') {
+		eventName = eventName.toString();
+	}
+
+	const currentTime = new Date();
+	const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;
+	console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`);
+}
+```
+
+Function that handles debug data.
+
+Example:
+
+```js
+const Emittery = require('emittery');
+
+const myLogger = (type, debugName, eventName, eventData) => console.log(`[${type}]: ${eventName}`);
+
+const emitter = new Emittery({
+	debug: {
+		name: 'myEmitter',
+		enabled: true,
+		logger: myLogger
+	}
+});
+
+emitter.on('test', data => {
+	// …
+});
+
+emitter.emit('test');
+//=> [subscribe]: test
+```
 
 #### on(eventName | eventName[], listener)
 
@@ -62,6 +211,7 @@ const emitter = new Emittery();
 emitter.on('🦄', data => {
 	console.log(data);
 });
+
 emitter.on(['🦄', '🐶'], data => {
 	console.log(data);
 });
@@ -114,6 +264,7 @@ const Emittery = require('emittery');
 const emitter = new Emittery();
 
 const listener = data => console.log(data);
+
 (async () => {
 	emitter.on(['🦄', '🐶', '🦊'], listener);
 	await emitter.emit('🦄', 'a');
@@ -144,6 +295,7 @@ emitter.once('🦄').then(data => {
 	console.log(data);
 	//=> '🌈'
 });
+
 emitter.once(['🦄', '🐶']).then(data => {
 	console.log(data);
 });
@@ -365,6 +517,14 @@ Listeners are not invoked for events emitted *before* the listener was added. Re
 
 Note that when using `.emitSerial()`, a slow listener will delay invocation of subsequent listeners. It's possible for newer events to overtake older ones.
 
+## Debugging
+
+Emittery can collect and log debug information.
+
+To enable this feature set the DEBUG environment variable to 'emittery' or '*'. Additionally you can set the static `isDebugEnabled` variable to true on the Emittery class, or `myEmitter.debug.enabled` on an instance of it for debugging a single instance.
+
+See [API](#api) for more details on how debugging works.
+
 ## FAQ
 
 ### How is this different than the built-in `EventEmitter` in Node.js?


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index e3c7e36..0386bbc 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -1,7 +1,7 @@
-/* eslint-disable @typescript-eslint/no-empty-function */
-import {expectType, expectError} from 'tsd';
+/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars, @typescript-eslint/no-floating-promises */
+import {expectType, expectError, expectNotAssignable, expectAssignable} from 'tsd';
 import pEvent = require('p-event');
-import Emittery = require('.');
+import Emittery = require('./index.js');
 
 type AnyListener = (eventData?: unknown) => void | Promise<void>;
 
@@ -19,6 +19,7 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
 	ee.on('anEvent', async () => Promise.resolve());
 	ee.on('anEvent', data => undefined);
 	ee.on('anEvent', async data => Promise.resolve());
+	ee.on(['anEvent', 'anotherEvent'], async data => undefined);
 	ee.on(Emittery.listenerAdded, ({eventName, listener}) => {
 		expectType<string | symbol | undefined>(eventName);
 		expectType<AnyListener>(listener);
@@ -66,6 +67,41 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
 	expectError(ee.on('anEvent', (data: any, more: any) => undefined));
 }
 
+// IsDebug
+{
+	type MyEventData = {
+		value: string;
+		open: undefined;
+		close: boolean;
+	};
+
+	const ee = new Emittery<MyEventData>();
+
+	const myLogger = (type: string, debugName: string, eventName?: keyof MyEventData, eventData?: MyEventData[keyof MyEventData]): void => {
+		expectAssignable<string>(type);
+		expectAssignable<string>(debugName);
+		expectAssignable<string | undefined>(eventName);
+		expectAssignable<MyEventData[keyof MyEventData]>(eventData);
+	};
+
+	const debugOptions = {name: 'test', enabled: true, logger: myLogger};
+
+	// Global debug flag
+	expectAssignable<boolean>(Emittery.isDebugEnabled);
+
+	// General debug options
+	expectAssignable<typeof ee.debug>(debugOptions);
+	expectAssignable<string>(ee.debug.name);
+	expectAssignable<boolean | undefined>(ee.debug.enabled);
+
+	// Debug logger
+	expectNotAssignable<() => undefined>(ee.debug.logger);
+	expectNotAssignable<(data: unknown) => undefined>(ee.debug.logger);
+	expectNotAssignable<(type: string, debugName: string) => undefined>(ee.debug.logger);
+	expectNotAssignable<((type: string, debugName: string, eventName?: string, eventData?: Record<string, any>) => void) | undefined>(ee.debug.logger);
+	expectAssignable<typeof ee.debug.logger>(myLogger);
+}
+
 // Userland can't emit the meta events
 {
 	const ee = new Emittery();
@@ -93,6 +129,7 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
 		value: string;
 		open: undefined;
 		close: undefined;
+		other: number;
 	}>();
 	ee.on('open', () => {});
 	ee.on('open', argument => {
@@ -103,13 +140,17 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
 	ee.on('value', argument => {
 		expectType<string>(argument);
 	});
-
+	ee.on(['value', 'other'], argument => {
+		expectType<string | number>(argument);
+	});
 	const listener = (value: string) => undefined;
 	ee.on('value', listener);
 	ee.off('value', listener);
 	const test = async () => {
 		const event = await ee.once('value');
 		expectType<string>(event);
+		const multiEvent = await ee.once(['value', 'other']);
+		expectType<string | number>(multiEvent);
 	};
 
 	expectError(ee.on('value', (value: number) => {}));
@@ -195,12 +236,17 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
 			value: string;
 			open: undefined;
 			close: undefined;
+			other: number;
 		}>();
 
 		for await (const event of ee.events('value')) {
 			expectType<string>(event);
 		}
 
+		for await (const event of ee.events(['value', 'other'])) {
+			expectType<string | number>(event);
+		}
+
 		for await (const event of ee.events(['value', 'open'])) {
 			expectType<string | undefined>(event);
 		}
@@ -212,25 +258,26 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
 	};
 }
 
+// TODO: Fix type compatibility with `p-event`.
 // Compatibility with p-event, without explicit types
-{
-	const ee = new Emittery();
-	pEvent.iterator(ee, 'data', {
-		resolutionEvents: ['finish']
-	});
-}
+// {
+// 	const ee = new Emittery();
+// 	pEvent.iterator(ee, 'data', {
+// 		resolutionEvents: ['finish']
+// 	});
+// }
 
 // Compatibility with p-event, with explicit types
-{
-	const ee = new Emittery<{
-		data: unknown;
-		error: unknown;
-		finish: undefined;
-	}>();
-	pEvent.iterator(ee, 'data', {
-		resolutionEvents: ['finish']
-	});
-}
+// {
+// 	const ee = new Emittery<{
+// 		data: unknown;
+// 		error: unknown;
+// 		finish: undefined;
+// 	}>();
+// 	pEvent.iterator(ee, 'data', {
+// 		resolutionEvents: ['finish']
+// 	});
+// }
 
 // Mixin type
 Emittery.mixin('emittery')(class {
diff --git a/test/index.js b/test/index.js
index 5d8001b..f37fb2b 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,7 +1,7 @@
 import test from 'ava';
 import delay from 'delay';
 import pEvent from 'p-event';
-import Emittery from '..';
+import Emittery from '../index.js';
 
 test('on()', async t => {
 	const emitter = new Emittery();
@@ -102,6 +102,27 @@ test('off() - listenerAdded', t => {
 	t.pass();
 });
 
+test('off() - isDebug logs output', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	const off = emitter.on('test', () => {});
+	off();
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'unsubscribe');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+});
+
 test('on() - listenerAdded offAny', async t => {
 	const emitter = new Emittery();
 	const addListener = () => 1;
@@ -161,6 +182,27 @@ test('on() - dedupes identical listeners', async t => {
 	t.deepEqual(calls, [1]);
 });
 
+test('on() - isDebug logs output', t => {
+	const eventStore = [];
+	const calls = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', data => calls.push(data));
+	t.true(eventStore.length > 0);
+	t.is(eventStore[0].type, 'subscribe');
+	t.is(eventStore[0].debugName, 'testEmitter');
+	t.is(eventStore[0].eventName, 'test');
+});
+
 test.serial('events()', async t => {
 	const emitter = new Emittery();
 	const iterator = emitter.events('🦄');
@@ -471,6 +513,28 @@ test('emit() - calls listeners subscribed when emit() was invoked', async t => {
 	t.deepEqual(calls, [1, 1, 2, 3, 2, 4, 5, 2, 4, 7, 6, 2, 4, 7, 6, 9, 2, 4, 7, 6, 9]);
 });
 
+test('emit() - isDebug logs output', async t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', () => {});
+	await emitter.emit('test', 'data');
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'emit');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+	t.is(eventStore[2].eventData, 'data');
+});
+
 test('emit() - returns undefined', async t => {
 	const emitter = new Emittery();
 
@@ -611,6 +675,28 @@ test('emitSerial() - calls listeners subscribed when emitSerial() was invoked',
 	t.deepEqual(calls, [1, 1, 2, 3, 2, 4, 5, 2, 4, 7, 6, 2, 4, 7, 6, 9, 2, 4, 7, 6, 9]);
 });
 
+test('emitSerial() - isDebug logs output', async t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', () => {});
+	await emitter.emitSerial('test', 'data');
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'emitSerial');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+	t.is(eventStore[2].eventData, 'data');
+});
+
 test('onAny()', async t => {
 	t.plan(4);
 
@@ -825,6 +911,68 @@ test('clearListeners() - with event name - clears iterators for that event', asy
 	t.deepEqual(await anyIterator.next(), {done: false, value: ['🌈', '💫']});
 });
 
+test('clearListeners() - isDebug logs output', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', () => {});
+	emitter.clearListeners('test');
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'clear');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+});
+
+test('onAny() - isDebug logs output', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.onAny(() => {});
+	t.true(eventStore.length > 0);
+	t.is(eventStore[0].type, 'subscribeAny');
+	t.is(eventStore[0].eventName, undefined);
+	t.is(eventStore[0].debugName, 'testEmitter');
+});
+
+test('offAny() - isDebug logs output', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	const off = emitter.onAny(() => {});
+	off();
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'unsubscribeAny');
+	t.is(eventStore[2].eventName, undefined);
+	t.is(eventStore[2].debugName, 'testEmitter');
+});
+
 test('listenerCount()', t => {
 	const emitter = new Emittery();
 	emitter.on('🦄', () => {});
@@ -875,7 +1023,7 @@ test('bindMethods()', t => {
 	const oldPropertyNames = Object.getOwnPropertyNames(target);
 	emitter.bindMethods(target, methodsToBind);
 
-	t.deepEqual(Object.getOwnPropertyNames(target).sort(), oldPropertyNames.concat(methodsToBind).sort());
+	t.deepEqual(Object.getOwnPropertyNames(target).sort(), [...oldPropertyNames, ...methodsToBind].sort());
 
 	for (const method of methodsToBind) {
 		t.is(typeof target[method], 'function');
@@ -911,7 +1059,7 @@ test('bindMethods() - methodNames must be array of strings or undefined', t => {
 });
 
 test('bindMethods() - must bind all methods if no array supplied', t => {
-	const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods'];
+	const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods', 'logIfDebugEnabled'];
 
 	const emitter = new Emittery();
 	const target = {};
@@ -919,7 +1067,7 @@ test('bindMethods() - must bind all methods if no array supplied', t => {
 	const oldPropertyNames = Object.getOwnPropertyNames(target);
 	emitter.bindMethods(target);
 
-	t.deepEqual(Object.getOwnPropertyNames(target).sort(), oldPropertyNames.concat(methodsExpected).sort());
+	t.deepEqual(Object.getOwnPropertyNames(target).sort(), [...oldPropertyNames, ...methodsExpected].sort());
 
 	for (const method of methodsExpected) {
 		t.is(typeof target[method], 'function');
@@ -978,13 +1126,13 @@ test('mixin() - methodNames must be array of strings or undefined', t => {
 });
 
 test('mixin() - must mixin all methods if no array supplied', t => {
-	const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods'];
+	const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods', 'logIfDebugEnabled'];
 
 	class TestClass {}
 
 	const TestClassWithMixin = Emittery.mixin('emitter')(TestClass);
 
-	t.deepEqual(Object.getOwnPropertyNames(TestClassWithMixin.prototype).sort(), methodsExpected.concat(['constructor', 'emitter']).sort());
+	t.deepEqual(Object.getOwnPropertyNames(TestClassWithMixin.prototype).sort(), [...methodsExpected, 'constructor', 'emitter'].sort());
 });
 
 test('mixin() - methodNames must only include Emittery methods', t => {
@@ -1008,3 +1156,57 @@ test('mixin() - target must be function', t => {
 	t.throws(() => Emittery.mixin('emitter')(undefined));
 	t.throws(() => Emittery.mixin('emitter')({}));
 });
+
+test('isDebug default logger handles symbol event names and object for event data', async t => {
+	const emitter = new Emittery({debug: {name: 'testEmitter', enabled: true}});
+	const eventName = Symbol('test');
+	emitter.on(eventName, () => {});
+	await t.notThrowsAsync(emitter.emit(eventName, {complex: ['data', 'structure', 1]}));
+});
+
+test('isDebug can be turned on globally during runtime', t => {
+	Emittery.isDebugEnabled = true;
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: false,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', () => {});
+	emitter.emit('test', 'test data');
+	Emittery.isDebugEnabled = false;
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'emit');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+	t.is(eventStore[2].eventData, 'test data');
+});
+
+test('isDebug can be turned on for and instance without using the constructor', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: false,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+	emitter.debug.enabled = true;
+
+	emitter.on('test', () => {});
+	emitter.emit('test', 'test data');
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'emit');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+	t.is(eventStore[2].eventData, 'test data');
+});