Skip to content

Commit 8f439b8

Browse files
authored
Stronger typing (#114)
* improved typing * upd readme * upd readme * upd test & readme * removed magic 1 * removed ts-toolbelt dependency
1 parent 22c5dcb commit 8f439b8

File tree

7 files changed

+139
-66
lines changed

7 files changed

+139
-66
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
package-lock.json
77
.DS_Store
88
.idea
9+
.vscode

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,17 @@ emitter.off('foo', onFoo) // unlisten
8080

8181
### Typescript
8282

83+
Set `"strict": true` in your tsconfig.json to get improved type inference for `mitt` instance methods.
84+
8385
```ts
8486
import mitt from 'mitt';
85-
const emitter: mitt.Emitter = mitt();
87+
88+
type Events = {
89+
foo: string
90+
bar?: number
91+
}
92+
93+
const emitter: mitt.Emitter<Events> = mitt<Events>();
8694
```
8795

8896
## Examples & Demos
@@ -126,7 +134,7 @@ Register an event handler for the given type.
126134

127135
#### Parameters
128136

129-
- `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
137+
- `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
130138
- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event
131139

132140
### off
@@ -135,15 +143,15 @@ Remove an event handler for the given type.
135143

136144
#### Parameters
137145

138-
- `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 `"*"`
146+
- `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 `'*'`
139147
- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler function to remove
140148

141149
### emit
142150

143151
Invoke all handlers for the given type.
144-
If present, `"*"` handlers are invoked after type-matched handlers.
152+
If present, `'*'` handlers are invoked after type-matched handlers.
145153

146-
Note: Manually firing "\*" handlers is not supported.
154+
Note: Manually firing '\*' handlers is not supported.
147155

148156
#### Parameters
149157

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"scripts": {
1313
"test": "npm-run-all --silent typecheck lint mocha test-types",
1414
"mocha": "mocha test",
15-
"test-types": "tsc test/test-types-compilation.ts --noEmit",
15+
"test-types": "tsc test/test-types-compilation.ts --noEmit --strict",
1616
"lint": "eslint src test --ext ts --ext js",
1717
"typecheck": "tsc --noEmit",
1818
"bundle": "microbundle",
@@ -78,7 +78,8 @@
7878
"@typescript-eslint/no-explicit-any": 0,
7979
"@typescript-eslint/explicit-function-return-type": 0,
8080
"@typescript-eslint/explicit-module-boundary-types": 0,
81-
"@typescript-eslint/no-empty-function": 0
81+
"@typescript-eslint/no-empty-function": 0,
82+
"@typescript-eslint/no-non-null-assertion": 0
8283
}
8384
},
8485
"eslintIgnore": [
@@ -104,6 +105,6 @@
104105
"sinon": "^9.0.2",
105106
"sinon-chai": "^3.5.0",
106107
"ts-node": "^8.10.2",
107-
"typescript": "^3.9.3"
108+
"typescript": "^3.9.7"
108109
}
109-
}
110+
}

src/index.ts

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,46 @@ export type EventType = string | symbol;
22

33
// An event handler can take an optional event argument
44
// and should not return a value
5-
export type Handler<T = any> = (event?: T) => void;
6-
export type WildcardHandler = (type: EventType, event?: any) => void;
5+
export type Handler<T = unknown> = (event: T) => void;
6+
export type WildcardHandler<T = Record<string, unknown>> = (
7+
type: keyof T,
8+
event: T[keyof T]
9+
) => void;
710

811
// An array of all currently registered event handlers for a type
9-
export type EventHandlerList = Array<Handler>;
10-
export type WildCardEventHandlerList = Array<WildcardHandler>;
12+
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
13+
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;
1114

1215
// A map of event types and their corresponding event handlers.
13-
export type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;
16+
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
17+
keyof Events | '*',
18+
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
19+
>;
1420

15-
export interface Emitter {
16-
all: EventHandlerMap;
21+
export interface Emitter<Events extends Record<EventType, unknown>> {
22+
all: EventHandlerMap<Events>;
1723

18-
on<T = any>(type: EventType, handler: Handler<T>): void;
19-
on(type: '*', handler: WildcardHandler): void;
24+
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
25+
on(type: '*', handler: WildcardHandler<Events>): void;
2026

21-
off<T = any>(type: EventType, handler: Handler<T>): void;
22-
off(type: '*', handler: WildcardHandler): void;
27+
off<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
28+
off(type: '*', handler: WildcardHandler<Events>): void;
2329

24-
emit<T = any>(type: EventType, event?: T): void;
25-
emit(type: '*', event?: any): void;
30+
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
31+
emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
2632
}
2733

2834
/**
2935
* Mitt: Tiny (~200b) functional event emitter / pubsub.
3036
* @name mitt
3137
* @returns {Mitt}
3238
*/
33-
export default function mitt(all?: EventHandlerMap): Emitter {
39+
export default function mitt<Events extends Record<EventType, unknown>>(
40+
all?: EventHandlerMap<Events>
41+
): Emitter<Events> {
42+
type GenericEventHandler =
43+
| Handler<Events[keyof Events]>
44+
| WildcardHandler<Events>;
3445
all = all || new Map();
3546

3647
return {
@@ -42,44 +53,52 @@ export default function mitt(all?: EventHandlerMap): Emitter {
4253

4354
/**
4455
* Register an event handler for the given type.
45-
* @param {string|symbol} type Type of event to listen for, or `"*"` for all events
56+
* @param {string|symbol} type Type of event to listen for, or `'*'` for all events
4657
* @param {Function} handler Function to call in response to given event
4758
* @memberOf mitt
4859
*/
49-
on<T = any>(type: EventType, handler: Handler<T>) {
50-
const handlers = all.get(type);
60+
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
61+
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
5162
const added = handlers && handlers.push(handler);
5263
if (!added) {
53-
all.set(type, [handler]);
64+
all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
5465
}
5566
},
5667

5768
/**
5869
* Remove an event handler for the given type.
59-
* @param {string|symbol} type Type of event to unregister `handler` from, or `"*"`
70+
* @param {string|symbol} type Type of event to unregister `handler` from, or `'*'`
6071
* @param {Function} handler Handler function to remove
6172
* @memberOf mitt
6273
*/
63-
off<T = any>(type: EventType, handler: Handler<T>) {
64-
const handlers = all.get(type);
74+
off<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
75+
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
6576
if (handlers) {
6677
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
6778
}
6879
},
6980

7081
/**
7182
* Invoke all handlers for the given type.
72-
* If present, `"*"` handlers are invoked after type-matched handlers.
83+
* If present, `'*'` handlers are invoked after type-matched handlers.
7384
*
74-
* Note: Manually firing "*" handlers is not supported.
85+
* Note: Manually firing '*' handlers is not supported.
7586
*
7687
* @param {string|symbol} type The event type to invoke
7788
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
7889
* @memberOf mitt
7990
*/
80-
emit<T = any>(type: EventType, evt: T) {
81-
((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });
82-
((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });
91+
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
92+
((all!.get(type) || []) as EventHandlerList<Events[keyof Events]>)
93+
.slice()
94+
.map((handler) => {
95+
handler(evt!);
96+
});
97+
((all!.get('*') || []) as WildCardEventHandlerList<Events>)
98+
.slice()
99+
.map((handler) => {
100+
handler(type, evt!);
101+
});
83102
}
84103
};
85104
}

test/index_test.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import mitt, { Emitter } from '..';
1+
import mitt, { Emitter, EventHandlerMap } from '..';
22
import chai, { expect } from 'chai';
33
import { spy } from 'sinon';
44
import sinonChai from 'sinon-chai';
@@ -15,17 +15,29 @@ describe('mitt', () => {
1515
const a = spy();
1616
const b = spy();
1717
map.set('foo', [a, b]);
18-
const events = mitt(map);
18+
const events = mitt<{ foo: undefined }>(map);
1919
events.emit('foo');
2020
expect(a).to.have.been.calledOnce;
2121
expect(b).to.have.been.calledOnce;
2222
});
2323
});
2424

2525
describe('mitt#', () => {
26-
let events, inst: Emitter;
27-
28-
beforeEach( () => {
26+
const eventType = Symbol('eventType');
27+
type Events = {
28+
foo: unknown;
29+
constructor: unknown;
30+
FOO: unknown;
31+
bar: unknown;
32+
Bar: unknown;
33+
'baz:bat!': unknown;
34+
'baz:baT!': unknown;
35+
Foo: unknown;
36+
[eventType]: unknown;
37+
};
38+
let events: EventHandlerMap<Events>, inst: Emitter<Events>;
39+
40+
beforeEach(() => {
2941
events = new Map();
3042
inst = mitt(events);
3143
});
@@ -83,7 +95,6 @@ describe('mitt#', () => {
8395

8496
it('can take symbols for event types', () => {
8597
const foo = () => {};
86-
const eventType = Symbol('eventType');
8798
inst.on(eventType, foo);
8899
expect(events.get(eventType)).to.deep.equal([foo]);
89100
});
@@ -151,7 +162,7 @@ describe('mitt#', () => {
151162
it('should invoke handler for type', () => {
152163
const event = { a: 'b' };
153164

154-
inst.on('foo', (one, two?) => {
165+
inst.on('foo', (one, two?: unknown) => {
155166
expect(one).to.deep.equal(event);
156167
expect(two).to.be.an('undefined');
157168
});

test/test-types-compilation.ts

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,77 @@
22

33
import mitt from '..';
44

5-
const emitter = mitt();
5+
interface SomeEventData {
6+
name: string;
7+
}
8+
9+
const emitter = mitt<{
10+
foo: string;
11+
someEvent: SomeEventData;
12+
bar?: number;
13+
}>();
14+
15+
const barHandler = (x?: number) => {};
16+
const fooHandler = (x: string) => {};
17+
const wildcardHandler = (
18+
_type: 'foo' | 'bar' | 'someEvent',
19+
_event: string | SomeEventData | number | undefined
20+
) => {};
621

722
/*
8-
* Check that if on is provided a generic, it only accepts handlers of that type
23+
* Check that 'on' args are inferred correctly
924
*/
1025
{
11-
const badHandler = (x: number) => {};
12-
const goodHandler = (x: string) => {};
26+
// @ts-expect-error
27+
emitter.on('foo', barHandler);
28+
emitter.on('foo', fooHandler);
1329

30+
emitter.on('bar', barHandler);
1431
// @ts-expect-error
15-
emitter.on<string>('foo', badHandler);
16-
emitter.on<string>('foo', goodHandler);
32+
emitter.on('bar', fooHandler);
33+
34+
emitter.on('*', wildcardHandler);
35+
// fooHandler is ok, because ('foo' | 'bar' | 'someEvent') extends string
36+
emitter.on('*', fooHandler);
37+
// @ts-expect-error
38+
emitter.on('*', barHandler);
1739
}
1840

1941
/*
20-
* Check that if off is provided a generic, it only accepts handlers of that type
42+
* Check that 'off' args are inferred correctly
2143
*/
2244
{
23-
const badHandler = (x: number) => {};
24-
const goodHandler = (x: string) => {};
45+
// @ts-expect-error
46+
emitter.off('foo', barHandler);
47+
emitter.off('foo', fooHandler);
2548

49+
emitter.off('bar', barHandler);
2650
// @ts-expect-error
27-
emitter.off<string>('foo', badHandler);
28-
emitter.off<string>('foo', goodHandler);
29-
}
51+
emitter.off('bar', fooHandler);
3052

53+
emitter.off('*', wildcardHandler);
54+
// fooHandler is ok, because ('foo' | 'bar' | 'someEvent') extends string
55+
emitter.off('*', fooHandler);
56+
// @ts-expect-error
57+
emitter.off('*', barHandler);
58+
}
3159

3260
/*
33-
* Check that if emitt is provided a generic, it only accepts event data of that type
61+
* Check that 'emit' args are inferred correctly
3462
*/
3563
{
36-
interface SomeEventData {
37-
name: string;
38-
}
39-
// @ts-expect-error
40-
emitter.emit<SomeEventData>('foo', 'NOT VALID');
41-
emitter.emit<SomeEventData>('foo', { name: 'jack' });
42-
}
64+
// @ts-expect-error
65+
emitter.emit('someEvent', 'NOT VALID');
66+
emitter.emit('someEvent', { name: 'jack' });
67+
68+
// @ts-expect-error
69+
emitter.emit('foo');
70+
// @ts-expect-error
71+
emitter.emit('foo', 1);
72+
emitter.emit('foo', 'string');
4373

74+
emitter.emit('bar');
75+
emitter.emit('bar', 1);
76+
// @ts-expect-error
77+
emitter.emit('bar', 'string');
78+
}

tsconfig.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
{
22
"compileOnSave": false,
33
"compilerOptions": {
4+
"strict": true,
45
"noEmit": true,
56
"declaration": true,
67
"moduleResolution": "node",
78
"esModuleInterop": true
89
},
9-
"include": [
10-
"src/*.ts",
11-
"test/*.ts",
12-
]
10+
"include": ["src/*.ts", "test/*.ts"]
1311
}

0 commit comments

Comments
 (0)