I have a function that accepts a Record
with event listener callbacks as values. I'd like to enforce that the first argument of an event callback within this record is typed as a CustomEvent<PayloadThatConformsToABaseType>
. I've tried the following:
type EventPayload = string | number;
interface CustomEvent<T> { payload: T }
function registerListeners<T extends EventPayload>(
listeners: Record<string, (e: CustomEvent<T>) => void>
) {}
// Gets inferred as `registerListeners<"hello">(listeners: Record<string, (e: CustomEvent<"hello">) => void>): void`
// Type '"hello"' is not assignable to type '2'.
registerListeners({
eventOne: (event: CustomEvent<'hello'>) => {},
eventTwo: (event: CustomEvent<2>) => {}
})
function registerListeners(
listeners: Record<string, <T extends EventPayload>(e: CustomEvent<T>) => void>
) {}
// Gets inferred as `registerListeners(listeners: Record<string, <T extends EventPayload>(e: CustomEvent<T>) => void>): void`
// Type 'EventPayload' is not assignable to type '"hello"'
registerListeners({
eventOne: (event: CustomEvent<'hello'>) => {},
eventTwo: (event: CustomEvent<2>) => {}
})
function registerListeners(listeners: Record<string, (e: CustomEvent<any>) => void>) {}
// Works but I can no longer constrain the event params
registerListeners({
eventOne: (event: CustomEvent<'hello'>) => {},
eventTwo: (event: CustomEvent<{a: string}>) => {}
})
How do I enforce that the callbacks passed to registerListeners
accept an acceptable event as argument?
It looks like you want to use a mapped type where the generic type parameter T
corresponds to the type argument to CustomEvent
for each key (e.g., for your example, the type argument for T
would be { eventOne: "hello"; eventTwo: 2; }
. Like this:
function registerListeners<T extends Record<keyof T, EventPayload>>(
listeners: { [K in keyof T]: (e: CustomEvent<T[K]>) => void }
) { }
Because this is a homomorphic mapped type (see What does "homomorphic mapped type" mean?) then the compiler knows how to infer from it when you call registerListeners()
:
registerListeners({
eventOne: (event: CustomEvent<'hello'>) => { },
eventTwo: (event: CustomEvent<2>) => { }
})
If you inspect with IntelliSense, you'll see that T
is inferred as { eventOne: "hello"; eventTwo: 2; }
as expected. Depending on the use case you could then go on to compute other types that depend on T
(in general the function might return something that needs to keep track of which events go with which keys).