diff --git a/.gitignore b/.gitignore index eb570d3..bf29d4f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ package-lock.json .DS_Store .idea +.vscode diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..84c52fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Jason Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9bc0436..dff9914 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,17 @@ emitter.off('foo', onFoo) // unlisten ### Typescript +Set `"strict": true` in your tsconfig.json to get improved type inference for `mitt` instance methods. + ```ts import mitt from 'mitt'; -const emitter: mitt.Emitter = mitt(); + +type Events = { + foo: string + bar?: number +} + +const emitter: mitt.Emitter = mitt(); ``` ## Examples & Demos @@ -126,7 +134,7 @@ Register an event handler for the given type. #### Parameters -- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `"*"` for all events +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `'*'` for all events - `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event ### off @@ -136,15 +144,15 @@ If omit the `handler`, all event handlers of the given type are deleted. #### Parameters -- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to unregister `handler` from, or `"*"` +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to unregister `handler` from, or `'*'` - `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler function to remove ### emit Invoke all handlers for the given type. -If present, `"*"` handlers are invoked after type-matched handlers. +If present, `'*'` handlers are invoked after type-matched handlers. -Note: Manually firing "\*" handlers is not supported. +Note: Manually firing '\*' handlers is not supported. #### Parameters diff --git a/package.json b/package.json index 51b5cf8..666ffe7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "test": "npm-run-all --silent typecheck lint mocha test-types", "mocha": "mocha test", - "test-types": "tsc test/test-types-compilation.ts --noEmit", + "test-types": "tsc test/test-types-compilation.ts --noEmit --strict", "lint": "eslint src test --ext ts --ext js", "typecheck": "tsc --noEmit", "bundle": "microbundle", @@ -34,7 +34,6 @@ ], "license": "MIT", "files": [ - "src", "dist", "index.d.ts" ], @@ -78,7 +77,8 @@ "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/explicit-module-boundary-types": 0, - "@typescript-eslint/no-empty-function": 0 + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-non-null-assertion": 0 } }, "eslintIgnore": [ @@ -104,6 +104,6 @@ "sinon": "^9.0.2", "sinon-chai": "^3.5.0", "ts-node": "^8.10.2", - "typescript": "^3.9.3" + "typescript": "^3.9.7" } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 946802a..4648156 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,27 +2,33 @@ export type EventType = string | symbol; // An event handler can take an optional event argument // and should not return a value -export type Handler = (event?: T) => void; -export type WildcardHandler = (type: EventType, event?: any) => void; +export type Handler = (event: T) => void; +export type WildcardHandler> = ( + type: keyof T, + event: T[keyof T] +) => void; // An array of all currently registered event handlers for a type -export type EventHandlerList = Array; -export type WildCardEventHandlerList = Array; +export type EventHandlerList = Array>; +export type WildCardEventHandlerList> = Array>; // A map of event types and their corresponding event handlers. -export type EventHandlerMap = Map; +export type EventHandlerMap> = Map< + keyof Events | '*', + EventHandlerList | WildCardEventHandlerList +>; -export interface Emitter { - all: EventHandlerMap; +export interface Emitter> { + all: EventHandlerMap; - on(type: EventType, handler: Handler): void; - on(type: '*', handler: WildcardHandler): void; + on(type: Key, handler: Handler): void; + on(type: '*', handler: WildcardHandler): void; - off(type: EventType, handler?: Handler): void; - off(type: '*', handler: WildcardHandler): void; + off(type: Key, handler?: Handler): void; + off(type: '*', handler: WildcardHandler): void; - emit(type: EventType, event?: T): void; - emit(type: '*', event?: any): void; + emit(type: Key, event: Events[Key]): void; + emit(type: undefined extends Events[Key] ? Key : never): void; } /** @@ -30,7 +36,12 @@ export interface Emitter { * @name mitt * @returns {Mitt} */ -export default function mitt(all?: EventHandlerMap): Emitter { +export default function mitt>( + all?: EventHandlerMap +): Emitter { + type GenericEventHandler = + | Handler + | WildcardHandler; all = all || new Map(); return { @@ -42,27 +53,27 @@ export default function mitt(all?: EventHandlerMap): Emitter { /** * Register an event handler for the given type. - * @param {string|symbol} type Type of event to listen for, or `"*"` for all events + * @param {string|symbol} type Type of event to listen for, or `'*'` for all events * @param {Function} handler Function to call in response to given event * @memberOf mitt */ - on(type: EventType, handler: Handler) { - const handlers = all.get(type); + on(type: Key, handler: GenericEventHandler) { + const handlers: Array | undefined = all!.get(type); const added = handlers && handlers.push(handler); if (!added) { - all.set(type, [handler]); + all!.set(type, [handler] as EventHandlerList); } }, /** * Remove an event handler for the given type. - * If omit the `handler`, all event handlers of the given type are deleted. - * @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` - * @param {Function} handler Handler function to remove + * If `handler` is omitted, all handlers of the given type are removed. + * @param {string|symbol} type Type of event to unregister `handler` from, or `'*'` + * @param {Function} [handler] Handler function to remove * @memberOf mitt */ - off(type: EventType, handler?: Handler) { - const handlers = all.get(type); + off(type: Key, handler?: GenericEventHandler) { + const handlers: Array | undefined = all!.get(type); if (handlers) { if (handler) { handlers.splice(handlers.indexOf(handler) >>> 0, 1); @@ -75,17 +86,32 @@ export default function mitt(all?: EventHandlerMap): Emitter { /** * Invoke all handlers for the given type. - * If present, `"*"` handlers are invoked after type-matched handlers. + * If present, `'*'` handlers are invoked after type-matched handlers. * - * Note: Manually firing "*" handlers is not supported. + * Note: Manually firing '*' handlers is not supported. * * @param {string|symbol} type The event type to invoke * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler * @memberOf mitt */ - emit(type: EventType, evt: T) { - ((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); }); - ((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); }); + emit(type: Key, evt?: Events[Key]) { + let handlers = all!.get(type); + if (handlers) { + (handlers as EventHandlerList) + .slice() + .map((handler) => { + handler(evt!); + }); + } + + handlers = all!.get('*'); + if (handlers) { + (handlers as WildCardEventHandlerList) + .slice() + .map((handler) => { + handler(type, evt!); + }); + } } }; } diff --git a/test/index_test.ts b/test/index_test.ts index 5c25b55..ba1dbff 100644 --- a/test/index_test.ts +++ b/test/index_test.ts @@ -1,4 +1,4 @@ -import mitt, { Emitter } from '..'; +import mitt, { Emitter, EventHandlerMap } from '..'; import chai, { expect } from 'chai'; import { spy } from 'sinon'; import sinonChai from 'sinon-chai'; @@ -15,7 +15,7 @@ describe('mitt', () => { const a = spy(); const b = spy(); map.set('foo', [a, b]); - const events = mitt(map); + const events = mitt<{ foo: undefined }>(map); events.emit('foo'); expect(a).to.have.been.calledOnce; expect(b).to.have.been.calledOnce; @@ -23,9 +23,21 @@ describe('mitt', () => { }); describe('mitt#', () => { - let events, inst: Emitter; - - beforeEach( () => { + const eventType = Symbol('eventType'); + type Events = { + foo: unknown; + constructor: unknown; + FOO: unknown; + bar: unknown; + Bar: unknown; + 'baz:bat!': unknown; + 'baz:baT!': unknown; + Foo: unknown; + [eventType]: unknown; + }; + let events: EventHandlerMap, inst: Emitter; + + beforeEach(() => { events = new Map(); inst = mitt(events); }); @@ -83,7 +95,6 @@ describe('mitt#', () => { it('can take symbols for event types', () => { const foo = () => {}; - const eventType = Symbol('eventType'); inst.on(eventType, foo); expect(events.get(eventType)).to.deep.equal([foo]); }); @@ -151,7 +162,7 @@ describe('mitt#', () => { it('should invoke handler for type', () => { const event = { a: 'b' }; - inst.on('foo', (one, two?) => { + inst.on('foo', (one, two?: unknown) => { expect(one).to.deep.equal(event); expect(two).to.be.an('undefined'); }); diff --git a/test/test-types-compilation.ts b/test/test-types-compilation.ts index 00510da..476e631 100644 --- a/test/test-types-compilation.ts +++ b/test/test-types-compilation.ts @@ -2,42 +2,77 @@ import mitt from '..'; -const emitter = mitt(); +interface SomeEventData { + name: string; +} + +const emitter = mitt<{ + foo: string; + someEvent: SomeEventData; + bar?: number; +}>(); + +const barHandler = (x?: number) => {}; +const fooHandler = (x: string) => {}; +const wildcardHandler = ( + _type: 'foo' | 'bar' | 'someEvent', + _event: string | SomeEventData | number | undefined +) => {}; /* - * Check that if on is provided a generic, it only accepts handlers of that type + * Check that 'on' args are inferred correctly */ { - const badHandler = (x: number) => {}; - const goodHandler = (x: string) => {}; + // @ts-expect-error + emitter.on('foo', barHandler); + emitter.on('foo', fooHandler); + emitter.on('bar', barHandler); // @ts-expect-error - emitter.on('foo', badHandler); - emitter.on('foo', goodHandler); + emitter.on('bar', fooHandler); + + emitter.on('*', wildcardHandler); + // fooHandler is ok, because ('foo' | 'bar' | 'someEvent') extends string + emitter.on('*', fooHandler); + // @ts-expect-error + emitter.on('*', barHandler); } /* - * Check that if off is provided a generic, it only accepts handlers of that type + * Check that 'off' args are inferred correctly */ { - const badHandler = (x: number) => {}; - const goodHandler = (x: string) => {}; + // @ts-expect-error + emitter.off('foo', barHandler); + emitter.off('foo', fooHandler); + emitter.off('bar', barHandler); // @ts-expect-error - emitter.off('foo', badHandler); - emitter.off('foo', goodHandler); -} + emitter.off('bar', fooHandler); + emitter.off('*', wildcardHandler); + // fooHandler is ok, because ('foo' | 'bar' | 'someEvent') extends string + emitter.off('*', fooHandler); + // @ts-expect-error + emitter.off('*', barHandler); +} /* - * Check that if emitt is provided a generic, it only accepts event data of that type + * Check that 'emit' args are inferred correctly */ { - interface SomeEventData { - name: string; - } - // @ts-expect-error - emitter.emit('foo', 'NOT VALID'); - emitter.emit('foo', { name: 'jack' }); -} + // @ts-expect-error + emitter.emit('someEvent', 'NOT VALID'); + emitter.emit('someEvent', { name: 'jack' }); + + // @ts-expect-error + emitter.emit('foo'); + // @ts-expect-error + emitter.emit('foo', 1); + emitter.emit('foo', 'string'); + emitter.emit('bar'); + emitter.emit('bar', 1); + // @ts-expect-error + emitter.emit('bar', 'string'); +} diff --git a/tsconfig.json b/tsconfig.json index acab4f5..563d58e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,11 @@ { "compileOnSave": false, "compilerOptions": { + "strict": true, "noEmit": true, "declaration": true, "moduleResolution": "node", "esModuleInterop": true }, - "include": [ - "src/*.ts", - "test/*.ts", - ] + "include": ["src/*.ts", "test/*.ts"] }