I am attempting to have TypeScript strictly type a pub/sub module, so that it yells if users pass payloads that don't adhere to their EVENT_NAME.
My first try was as follows:
enum EVENT_NAME {
CLICK = 'CLICK',
MOVE = 'MOVE'
}
type PAYLOADS = {
[EVENT_NAME.CLICK]: {
value: string
},
[EVENT_NAME.MOVE]: {
distance: number
}
}
const subscribers: Map<EVENT_NAME, Set<(value: PAYLOADS[EVENT_NAME]) => void>> = new Map();
function on<E extends EVENT_NAME>(eventName: E, listener: (value: PAYLOADS[E]) => void) {
if (!subscribers.has(eventName)) {
subscribers.set(eventName, new Set());
} else {
// Without the assertion TypeScript yells that:
/**
* Argument of type '(value: PAYLOADS[E]) => void' is not assignable to parameter of type '(value: { value: string; } | { distance: number; }) => void'.
Types of parameters 'value' and 'value' are incompatible.
Type '{ value: string; } | { distance: number; }' is not assignable to type 'PAYLOADS[E]'.
Type '{ value: string; }' is not assignable to type 'PAYLOADS[E]'.
Type '{ value: string; }' is not assignable to type '{ value: string; } & { distance: number; }'.
Property 'distance' is missing in type '{ value: string; }' but required in type '{ distance: number; }'.(2345)
*/
// Is it possible to avoid this type assertion? Or is it the only way?
const set = subscribers.get(eventName) as Set<(value: PAYLOADS[E]) => void>;
set.add(listener);
}
}
on(EVENT_NAME.CLICK, ({ value }) => {
console.log(value)
});
on(EVENT_NAME.MOVE, ({ distance }) => {
console.log(distance);
});
As I understand it, the map will be created where EVENT_NAME and PAYLOAD aren't co-dependent, so I tried using an intersection type for the map, and overloads for the on
function:
const subscribers: Map<EVENT_NAME.CLICK, Set<(value: PAYLOADS[EVENT_NAME.CLICK]) => void>> & Map<EVENT_NAME.MOVE, Set<(value: PAYLOADS[EVENT_NAME.MOVE]) => void>> = new Map();
function on<E extends EVENT_NAME.CLICK>(eventName: E, listener: (value: PAYLOADS[E]) => void): void;
function on<E extends EVENT_NAME.MOVE>(eventName: E, listener: (value: PAYLOADS[E]) => void): void;
function on(eventName: any, listener: (value: any) => void) {
if (!subscribers.has(eventName)) {
subscribers.set(eventName, new Set());
} else {
const set = subscribers.get(eventName)!;
set.add(listener);
}
}
on(EVENT_NAME.CLICK, ({ value }) => {
console.log(value);
});
on(EVENT_NAME.MOVE, ({ distance }) => {
console.log(distance);
});
With overloads, everything works, but is quite verbose (there could be dozens of EVENT_NAMES), also types are lost inside the on
function Also, adding each event to the Map intersection type feels overly verbose. Is there a better way I am missing? Thanks in advance!
Edit: Adding codesandbox links and fixed code so it runs: