Skip to content

Commit 01e4ade

Browse files
committed
feat: enhance EventEmitter typing and remove unused index signatures for improved type safety
1 parent 36f7699 commit 01e4ade

5 files changed

Lines changed: 71 additions & 46 deletions

File tree

src/download/download-engine/download-file/download-engine-file.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export type DownloadEngineFileEvents = {
4141
save: (progress: SaveProgressInfo) => void;
4242
finished: () => void;
4343
closed: () => void;
44-
[key: string]: any;
4544
};
4645

4746
const DEFAULT_CHUNKS_SIZE_FOR_CHUNKS_PROGRAM = 1024 * 1024 * 5; // 5MB

src/download/download-engine/engine/base-download-engine.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ export type BaseDownloadEngineEvents = {
4949
save: (progress: SaveProgressInfo) => void;
5050
finished: () => void;
5151
closed: () => void;
52-
[key: string]: any;
5352
};
5453

5554
export const DEFAULT_BASE_DOWNLOAD_ENGINE_OPTIONS: Partial<BaseDownloadEngineOptions> = {

src/download/download-engine/engine/download-engine-multi-download.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ import {DownloadFlags, DownloadStatus} from "../download-file/progress-status-fi
77
import {DownloadEngineRemote} from "./DownloadEngineRemote.js";
88
import {promiseWithResolvers} from "../utils/promiseWithResolvers.js";
99

10-
export type DownloadEngineMultiAllowedEngines = BaseDownloadEngine | DownloadEngineRemote | DownloadEngineMultiDownload<any>;
10+
type BaseDownloadEngineEventName = Extract<keyof BaseDownloadEngineEvents, string>;
11+
12+
export type BaseDownloadEngineEventTarget = {
13+
on<Key extends BaseDownloadEngineEventName>(eventName: Key, listener: BaseDownloadEngineEvents[Key]): unknown;
14+
off<Key extends BaseDownloadEngineEventName>(eventName: Key, listener: BaseDownloadEngineEvents[Key]): unknown;
15+
once<Key extends BaseDownloadEngineEventName>(eventName: Key, listener: BaseDownloadEngineEvents[Key]): unknown;
16+
};
17+
18+
type DownloadEngineMultiAllowedEngineCore = BaseDownloadEngine | DownloadEngineRemote | DownloadEngineMultiDownload<any>;
19+
export type DownloadEngineMultiAllowedEngines = DownloadEngineMultiAllowedEngineCore & BaseDownloadEngineEventTarget;
1120

1221
type DownloadEngineMultiDownloadEvents<Engine = DownloadEngineMultiAllowedEngines> = BaseDownloadEngineEvents & {
1322
childDownloadStarted: (engine: Engine) => void

src/utils/EventEmitter.ts

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
type AnyListener = (...args: any[]) => void;
22

3-
type KnownKeys<T> = {
4-
[Key in keyof T]: string extends Key ? never : number extends Key ? never : symbol extends Key ? never : Key;
5-
}[keyof T];
6-
7-
type KnownEventName<Events extends object> = Extract<KnownKeys<Events>, string | symbol>;
8-
type KnownEventListener<Events extends object, Key extends KnownEventName<Events>> = Events[Key] extends AnyListener ? Events[Key] : never;
93
type ListenerArgs<Listener> = Listener extends (...args: infer Args) => void ? Args : never;
104
type StoredListener = AnyListener & {
115
_originalListener?: AnyListener;
126
};
137
type StoredEvent = StoredListener | StoredListener[];
148
type EventName = string | symbol;
9+
type KnownEventName<Events extends object> = Extract<{
10+
[Key in keyof Events]: Key extends EventName
11+
? string extends Key
12+
? never
13+
: number extends Key
14+
? never
15+
: symbol extends Key
16+
? never
17+
: Key
18+
: never;
19+
}[keyof Events], EventName>;
20+
type EventListener<Events extends object, Key extends EventName> = Key extends keyof Events ? Events[Key] extends AnyListener ? Events[Key] : never : AnyListener;
21+
type EventListenerArgs<Events extends object, Key extends EventName> = ListenerArgs<EventListener<Events, Key>>;
22+
type SuggestedEventName<Events extends object, ExtraEvents extends EventName> = KnownEventName<Events> | (ExtraEvents & {});
23+
type HasBroadEventKeys<Events extends object> = string extends keyof Events ? true : symbol extends keyof Events ? true : false;
24+
type DefaultExtraEvents<Events extends object> = HasBroadEventKeys<Events> extends true ? EventName : never;
1525

1626
export type EventMap = Record<PropertyKey, AnyListener>;
1727

18-
export class EventEmitter<Events extends object = Record<string, never>> {
28+
export class EventEmitter<Events extends object = EventMap, ExtraEvents extends EventName = DefaultExtraEvents<Events>> {
1929
private _events = new Map<EventName, StoredEvent>();
2030

21-
public on<Key extends KnownEventName<Events>>(eventName: Key, listener: KnownEventListener<Events, Key>): this;
22-
public on(eventName: EventName, listener: AnyListener): this;
23-
public on(eventName: EventName, listener: AnyListener): this {
31+
public on<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key, listener: EventListener<Events, Key>): this {
2432
const current = this._events.get(eventName);
2533

2634
if (current === undefined) {
@@ -37,27 +45,21 @@ export class EventEmitter<Events extends object = Record<string, never>> {
3745
return this;
3846
}
3947

40-
public addListener<Key extends KnownEventName<Events>>(eventName: Key, listener: KnownEventListener<Events, Key>): this;
41-
public addListener(eventName: EventName, listener: AnyListener): this;
42-
public addListener(eventName: EventName, listener: AnyListener): this {
48+
public addListener<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key, listener: EventListener<Events, Key>): this {
4349
return this.on(eventName, listener);
4450
}
4551

46-
public once<Key extends KnownEventName<Events>>(eventName: Key, listener: KnownEventListener<Events, Key>): this;
47-
public once(eventName: EventName, listener: AnyListener): this;
48-
public once(eventName: EventName, listener: AnyListener): this {
52+
public once<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key, listener: EventListener<Events, Key>): this {
4953
const onceListener: StoredListener = (...args) => {
50-
this.off(eventName, onceListener);
54+
this.off(eventName, onceListener as EventListener<Events, Key>);
5155
listener(...args);
5256
};
5357

5458
onceListener._originalListener = listener;
55-
return this.on(eventName, onceListener);
59+
return this.on(eventName, onceListener as EventListener<Events, Key>);
5660
}
5761

58-
public off<Key extends KnownEventName<Events>>(eventName: Key, listener: KnownEventListener<Events, Key>): this;
59-
public off(eventName: EventName, listener: AnyListener): this;
60-
public off(eventName: EventName, listener: AnyListener): this {
62+
public off<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key, listener: EventListener<Events, Key>): this {
6163
const current = this._events.get(eventName);
6264

6365
if (current === undefined) {
@@ -98,9 +100,7 @@ export class EventEmitter<Events extends object = Record<string, never>> {
98100
return this;
99101
}
100102

101-
public removeListener<Key extends KnownEventName<Events>>(eventName: Key, listener: KnownEventListener<Events, Key>): this;
102-
public removeListener(eventName: EventName, listener: AnyListener): this;
103-
public removeListener(eventName: EventName, listener: AnyListener): this {
103+
public removeListener<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key, listener: EventListener<Events, Key>): this {
104104
return this.off(eventName, listener);
105105
}
106106

@@ -114,9 +114,7 @@ export class EventEmitter<Events extends object = Record<string, never>> {
114114
return this;
115115
}
116116

117-
public emit0<Key extends KnownEventName<Events>>(eventName: Key): boolean;
118-
public emit0(eventName: EventName): boolean;
119-
public emit0(eventName: EventName): boolean {
117+
public emit0<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key): boolean {
120118
const current = this._events.get(eventName);
121119

122120
if (current === undefined) {
@@ -151,9 +149,7 @@ export class EventEmitter<Events extends object = Record<string, never>> {
151149
return true;
152150
}
153151

154-
public emit1<Key extends KnownEventName<Events>>(eventName: Key, a1: ListenerArgs<KnownEventListener<Events, Key>>[0]): boolean;
155-
public emit1(eventName: EventName, a1: any): boolean;
156-
public emit1(eventName: EventName, a1: any): boolean {
152+
public emit1<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key, a1: EventListenerArgs<Events, Key>[0]): boolean {
157153
const current = this._events.get(eventName);
158154

159155
if (current === undefined) {
@@ -188,9 +184,7 @@ export class EventEmitter<Events extends object = Record<string, never>> {
188184
return true;
189185
}
190186

191-
public emit2<Key extends KnownEventName<Events>>(eventName: Key, a1: ListenerArgs<KnownEventListener<Events, Key>>[0], a2: ListenerArgs<KnownEventListener<Events, Key>>[1]): boolean;
192-
public emit2(eventName: EventName, a1: any, a2: any): boolean;
193-
public emit2(eventName: EventName, a1: any, a2: any): boolean {
187+
public emit2<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key, a1: EventListenerArgs<Events, Key>[0], a2: EventListenerArgs<Events, Key>[1]): boolean {
194188
const current = this._events.get(eventName);
195189

196190
if (current === undefined) {
@@ -225,9 +219,7 @@ export class EventEmitter<Events extends object = Record<string, never>> {
225219
return true;
226220
}
227221

228-
public emit<Key extends KnownEventName<Events>>(eventName: Key, ...args: ListenerArgs<KnownEventListener<Events, Key>>): boolean;
229-
public emit(eventName: EventName, ...args: any[]): boolean;
230-
public emit(eventName: EventName, ...args: any[]): boolean {
222+
public emit<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key, ...args: EventListenerArgs<Events, Key>): boolean {
231223
switch (args.length) {
232224
case 0:
233225
return this.emit0(eventName);
@@ -270,22 +262,20 @@ export class EventEmitter<Events extends object = Record<string, never>> {
270262
return Array.from(this._events.keys());
271263
}
272264

273-
public listeners<Key extends KnownEventName<Events>>(eventName: Key): KnownEventListener<Events, Key>[];
274-
public listeners(eventName: EventName): AnyListener[];
275-
public listeners(eventName: EventName): AnyListener[] {
265+
public listeners<Key extends SuggestedEventName<Events, ExtraEvents>>(eventName: Key): EventListener<Events, Key>[] {
276266
const current = this._events.get(eventName);
277267

278268
if (current === undefined) {
279269
return [];
280270
}
281271

282272
if (!Array.isArray(current)) {
283-
return [this._unwrapListener(current)];
273+
return [this._unwrapListener(current) as EventListener<Events, Key>];
284274
}
285275

286-
const listeners = new Array<AnyListener>(current.length);
276+
const listeners = new Array<EventListener<Events, Key>>(current.length);
287277
for (let index = 0; index < current.length; index++) {
288-
listeners[index] = this._unwrapListener(current[index]);
278+
listeners[index] = this._unwrapListener(current[index]) as EventListener<Events, Key>;
289279
}
290280

291281
return listeners;

test/eventEmitter.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import {describe, expect, test, vi} from "vitest";
22
import {EventEmitter} from "../src/utils/EventEmitter.js";
33

4+
type Assert<T extends true> = T;
5+
type IsAny<T> = 0 extends (1 & T) ? true : false;
6+
type IsExact<T, Expected> = [T] extends [Expected] ? ([Expected] extends [T] ? true : false) : false;
7+
48
type TestEvents = {
59
data: (value: number, label: string) => void;
610
closed: () => void;
7-
[key: string]: any;
811
};
912

1013
describe("EventEmitter", () => {
@@ -45,6 +48,10 @@ describe("EventEmitter", () => {
4548
const emitter = new EventEmitter<TestEvents>();
4649

4750
emitter.on("data", (value, label) => {
51+
type ValueIsNotAny = Assert<IsAny<typeof value> extends false ? true : false>;
52+
type LabelIsNotAny = Assert<IsAny<typeof label> extends false ? true : false>;
53+
type ValueIsNumber = Assert<IsExact<typeof value, number>>;
54+
type LabelIsString = Assert<IsExact<typeof label, string>>;
4855
const typedValue: number = value;
4956
const typedLabel: string = label;
5057

@@ -53,7 +60,18 @@ describe("EventEmitter", () => {
5360
});
5461

5562
const typedListeners: Array<(value: number, label: string) => void> = emitter.listeners("data");
63+
type TypedListenersAreExact = Assert<IsExact<typeof typedListeners, Array<(value: number, label: string) => void>>>;
64+
emitter.emit("data", 1, "ok");
65+
66+
// @ts-expect-error data listeners receive a number as the first argument
67+
emitter.on("data", (value: string) => {
68+
void value;
69+
});
70+
71+
// @ts-expect-error data emits require a string label as the second argument
72+
emitter.emit("data", 1, 2);
5673

74+
void (true as TypedListenersAreExact);
5775
expect(typedListeners).toHaveLength(1);
5876
});
5977

@@ -68,6 +86,16 @@ describe("EventEmitter", () => {
6886
expect(emitter.eventNames()).toContain(eventName);
6987
});
7088

89+
test("supports unknown event names without affecting known event typing", () => {
90+
const emitter = new EventEmitter<TestEvents, string | symbol>();
91+
const listener = vi.fn();
92+
93+
emitter.on("custom", listener);
94+
95+
expect(emitter.emit("custom", 1, "label")).toBe(true);
96+
expect(listener).toHaveBeenCalledWith(1, "label");
97+
});
98+
7199
test("keeps emit order stable when listeners remove each other", () => {
72100
const emitter = new EventEmitter<TestEvents>();
73101
const calls: string[] = [];

0 commit comments

Comments
 (0)