Skip to content

Instantly share code, notes, and snippets.

@stepankuzmin
Last active July 10, 2024 15:23
Show Gist options
  • Save stepankuzmin/f5393e8cb32df069146a4351581d2bd0 to your computer and use it in GitHub Desktop.
Save stepankuzmin/f5393e8cb32df069146a4351581d2bd0 to your computer and use it in GitHub Desktop.
Strongly Typed Evented class with on() and fire()
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