Reputation: 249
I try to allow my function consumers to user wider type but I use this type in contravariant position and typescript complaints.
Simplified code:
function wrapper<T extends Event = Event>(node: HTMLElement) {
const handler = (e: T) => console.log(e.cancelable);
node.addEventListener("click", handler);
}
And I get error:
Type 'Event' is not assignable to type 'T'. 'Event' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Event'.
How can I express my intent so typescript would understand me?
Full code:
export function tracker(
eventType: string,
options?: boolean | AddEventListenerOptions
) {
const stream: Stream<Event> = Stream.empty();
function tracker(node: HTMLElement) {
const handler = (event: Event) => stream.shamefullySendNext(event);
node.addEventListener( eventType, handler, options);
return {
destroy: () => {
node.removeEventListener(eventType, handler);
stream.shamefullySendComplete();
},
};
}
return {
stream,
tracker,
};
}
Upvotes: 0
Views: 479
Reputation: 42170
Your question is quite confusing, but I believe that what you want is to refine based on the event type (ie. "click") rather than the event.
We cannot ever safely use a generic T
that extends Event
because this T
could have any arbitrary additional properties. Consider the following example:
interface MyEvent extends Event {
customProperty: string
};
const handler = (e: MyEvent) => console.log(e.cancelable);
No event listener would ever be able to execute the callback handler
because their event
lacks the property customProperty
. That's why you get the error that "Type 'Event' is not assignable to type 'T'."
We can however refine on event type because this is what addEventListener
does.
addEventListener<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
): void;
addEventListener
can be called with a generic specifying the event type name. When that generic K
is set, the type of the event that is passed as an argument to the callback is refined based on the HTMLElementEventMap
.
We can make your tracker
function depend on the same generic <K extends keyof HTMLElementEventMap>
. Now your eventType
must be K
and your handler must take (event: HTMLElementEventMap[K])
. Notice that we have narrowed the event, but we haven't allowed for extra properties to be added via extends
.
export function tracker<K extends keyof HTMLElementEventMap>(
eventType: K,
options?: boolean | AddEventListenerOptions
) {
const stream: Stream<HTMLElementEventMap[K]> = Stream.empty();
function tracker(node: HTMLElement) {
const handler = (event: HTMLElementEventMap[K]) => stream.shamefullySendNext(event);
node.addEventListener<K>( eventType, handler, options);
return {
destroy: () => {
node.removeEventListener(eventType, handler);
stream.shamefullySendComplete();
},
};
}
return {
stream,
tracker,
};
}
Upvotes: 1