Skip to content

Commit

Permalink
Stronger typing (#114)
Browse files Browse the repository at this point in the history
* improved typing

* upd readme

* upd readme

* upd test & readme

* removed magic 1

* removed ts-toolbelt dependency
  • Loading branch information
iyegoroff authored Jun 22, 2021
1 parent 22c5dcb commit 8f439b8
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 66 deletions.
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"]
}

0 comments on commit 8f439b8

Please sign in to comment.