Last active
July 10, 2024 15:23
-
-
Save stepankuzmin/f5393e8cb32df069146a4351581d2bd0 to your computer and use it in GitHub Desktop.
Strongly Typed Evented class with on() and fire()
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type EventData = { | |
[key: string]: unknown; | |
}; | |
export class Event<Type extends string = string, Data extends EventData | void = void> { | |
target: unknown; | |
readonly type: Type; | |
/** | |
* Virtual property to ensure that events with different data types are not compatible. | |
* | |
* @private | |
* @example | |
* new Event('click', {required: true}) satisfies Event<'click', {required: boolean}>; | |
* | |
* @example | |
* // @ts-expect-error - Property 'required' is missing in type '{}' but required in type '{ required: boolean; }' | |
* new Event('click', {}) satisfies Event<'click', {required: boolean}>; | |
*/ | |
private _eventData: Data; | |
constructor(type: Type, _eventData: Data = {} as Data) { | |
this.type = type; | |
} | |
} | |
export type Listener<T = Event> = (e: T) => void | |
/** | |
* Utility type that represents a registry of events. Maps event type to an event object. | |
*/ | |
type EventRegistry = Record<string, Event<string, EventData | void>>; | |
type GenericEventRegistry = Record<string, Event<string, EventData>>; | |
/** | |
* Utility type that extracts the event data type from the event. | |
*/ | |
type EventDataOf<E> = E extends Event<string, infer D> ? D : never; | |
/** | |
* Utility type that extracts the event type and extends it with the event data type if present. | |
*/ | |
type EventOf<E> = | |
E extends Event<infer T, infer D> ? (D extends void ? Event<T, void> : Event<T, D> & D) : never; | |
export class Evented<R extends EventRegistry = GenericEventRegistry> { | |
on<T extends keyof R & string>(type: T, listener: Listener<EventOf<R[T]>>): this { | |
return this; | |
} | |
fire<T extends keyof R>(event: R[T]): this; | |
fire<T extends keyof R>(type: T, eventData?: EventDataOf<R[T]>): this; | |
fire<T extends keyof R>(event: EventOf<R[T]> | T, eventData?: EventDataOf<R[T]>): this { | |
return this; | |
} | |
} | |
type Registry = { | |
'payload': Event<'payload', {prop1: string, prop2: string}>; | |
'no-payload': Event<'no-payload'>; | |
}; | |
export const typedEvented = new Evented<Registry>(); | |
// @ts-expect-error - eventData must extend object | |
new Event('test', false); | |
typedEvented.fire(new Event('payload', {prop1: '', prop2: ''})); | |
// @ts-expect-error | |
typedEvented.fire(new Event('payload', {___prop1: '', ___prop2: ''})); | |
typedEvented.fire(new Event('no-payload')); | |
// @ts-expect-error | |
typedEvented.fire(new Event('no-payload', {___prop1: '', ___prop2: ''})); | |
// @ts-expect-error | |
typedEvented.fire(new Event('not-existant')); | |
typedEvented.fire('payload', {prop1: '', prop2: ''}); | |
// @ts-expect-error | |
typedEvented.fire('payload', {prop12: '', prop22: ''}); | |
typedEvented.fire('no-payload'); | |
// @ts-expect-error | |
typedEvented.fire('no-payload', {a: 42}); | |
// @ts-expect-error | |
typedEvented.fire('not-existant'); | |
typedEvented.on('payload', (e) => { | |
console.log(e.prop2); | |
console.log(e.prop1); | |
// @ts-expect-error | |
console.log(e.nothing); | |
}); | |
typedEvented.on('no-payload', (e) => { | |
console.log(e.type); | |
// @ts-expect-error | |
console.log(e.nothing); | |
}); | |
// ------------------------------ | |
export const genericEvented = new Evented(); | |
genericEvented.on('data', (e) => { | |
console.log(e.type); | |
console.log(e.target); | |
console.log(e.nothing); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment