Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stronger typing #114

Merged
merged 6 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
package-lock.json
.DS_Store
.idea
.vscode
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Events> = mitt<Events>();
```

## Examples & Demos
Expand Down Expand Up @@ -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
Expand All @@ -135,15 +143,15 @@ Remove 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 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

Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -78,7 +78,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": [
Expand All @@ -104,6 +105,6 @@
"sinon": "^9.0.2",
"sinon-chai": "^3.5.0",
"ts-node": "^8.10.2",
"typescript": "^3.9.3"
"typescript": "^3.9.7"
}
}
}
71 changes: 45 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,46 @@ export type EventType = string | symbol;

// An event handler can take an optional event argument
// and should not return a value
export type Handler<T = any> = (event?: T) => void;
export type WildcardHandler = (type: EventType, event?: any) => void;
export type Handler<T = unknown> = (event: T) => void;
export type WildcardHandler<T = Record<string, unknown>> = (
type: keyof T,
event: T[keyof T]
) => void;

// An array of all currently registered event handlers for a type
export type EventHandlerList = Array<Handler>;
export type WildCardEventHandlerList = Array<WildcardHandler>;
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;

// A map of event types and their corresponding event handlers.
export type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
keyof Events | '*',
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
>;

export interface Emitter {
all: EventHandlerMap;
export interface Emitter<Events extends Record<EventType, unknown>> {
all: EventHandlerMap<Events>;

on<T = any>(type: EventType, handler: Handler<T>): void;
on(type: '*', handler: WildcardHandler): void;
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
on(type: '*', handler: WildcardHandler<Events>): void;

off<T = any>(type: EventType, handler: Handler<T>): void;
off(type: '*', handler: WildcardHandler): void;
off<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
off(type: '*', handler: WildcardHandler<Events>): void;

emit<T = any>(type: EventType, event?: T): void;
emit(type: '*', event?: any): void;
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
}

/**
* Mitt: Tiny (~200b) functional event emitter / pubsub.
* @name mitt
* @returns {Mitt}
*/
export default function mitt(all?: EventHandlerMap): Emitter {
export default function mitt<Events extends Record<EventType, unknown>>(
all?: EventHandlerMap<Events>
): Emitter<Events> {
type GenericEventHandler =
| Handler<Events[keyof Events]>
| WildcardHandler<Events>;
all = all || new Map();

return {
Expand All @@ -42,44 +53,52 @@ 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<T = any>(type: EventType, handler: Handler<T>) {
const handlers = all.get(type);
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
const added = handlers && handlers.push(handler);
if (!added) {
all.set(type, [handler]);
all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
}
},

/**
* Remove an event handler for the given type.
* @param {string|symbol} type Type of event to unregister `handler` from, or `"*"`
* @param {string|symbol} type Type of event to unregister `handler` from, or `'*'`
* @param {Function} handler Handler function to remove
* @memberOf mitt
*/
off<T = any>(type: EventType, handler: Handler<T>) {
const handlers = all.get(type);
off<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
}
},

/**
* 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<T = any>(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<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
((all!.get(type) || []) as EventHandlerList<Events[keyof Events]>)
.slice()
.map((handler) => {
handler(evt!);
});
((all!.get('*') || []) as WildCardEventHandlerList<Events>)
.slice()
.map((handler) => {
handler(type, evt!);
});
}
};
}
25 changes: 18 additions & 7 deletions test/index_test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,17 +15,29 @@ 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;
});
});

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<Events>, inst: Emitter<Events>;

beforeEach(() => {
events = new Map();
inst = mitt(events);
});
Expand Down Expand Up @@ -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]);
});
Expand Down Expand Up @@ -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');
});
Expand Down
75 changes: 55 additions & 20 deletions test/test-types-compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>('foo', badHandler);
emitter.on<string>('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<string>('foo', badHandler);
emitter.off<string>('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<SomeEventData>('foo', 'NOT VALID');
emitter.emit<SomeEventData>('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');
}
6 changes: 2 additions & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}