Description
openedon Feb 22, 2023
Bug Report
π Search Terms
- liskov
- addEventListener
- overload
π Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about everything. I've checked the full FAQ.
β― Playground Link
Playground link with relevant code
π» Code
interface FooBarEventMap {
click: CustomEvent;
}
class FooBarElement extends HTMLElement {
}
interface FooBarElement extends HTMLElement {
addEventListener<T extends keyof FooBarEventMap>(
type: T,
listener: (this: FooBarElement, ev: FooBarEventMap[T]) => any,
options?: boolean | AddEventListenerOptions,
): void;
}
interface HTMLElementTagNameMap {
"foo-bar": FooBarElement;
}
let one: NodeListOf<Node> = document.querySelectorAll('foo-bar')!;
one.forEach(el => el.addEventListener('click', null));
let two: NodeListOf<Node> = document.querySelectorAll('div')!;
two.forEach(el => el.addEventListener('click', null));
let three: NodeListOf<FooBarElement> = document.querySelectorAll('foo-bar')!;
three.forEach(el => el.addEventListener('click', null));
let four: NodeListOf<HTMLDivElement> = document.querySelectorAll('div')!;
four.forEach(el => el.addEventListener('click', null));
π Actual behavior
An error is reported for the assignment to one
.
This is technically correct: FooBarElement
is a subtype of HTMLElement
which in turn is a subtype of Node
. FooBarElement.addEventListener()
does not accept all parameters that are accepted by Node.addEventListener()
, making this a LSP violation.
However the same is true for HTMLDivElement
. The assignment to two
is accepted, but as evidenced by the assignment to four
, an HTMLDivElement
does not actually accept null
.
When I duplicate the addEventListener
overload to:
interface FooBarElement extends HTMLElement {
addEventListener<T extends keyof FooBarEventMap>(
type: T,
listener: (this: FooBarElement, ev: FooBarEventMap[T]) => any,
options?: boolean | AddEventListenerOptions,
): void;
addEventListener<T extends keyof FooBarEventMap>(
type: T,
listener: (this: FooBarElement, ev: FooBarEventMap[T]) => any,
options?: boolean | AddEventListenerOptions,
): void;
}
β¦ both overloads are 100% identical. Then the assignment to one
is no longer reported as an error.
π Expected behavior
I would not expect this inconsistent behavior:
- Either both HTMLDivElement and FooBarElement should report an error when assigning to a
NodeListOf<Node>
, or neither should. - Adding an additional identical overload should not make the error go away.
Additional context
The real world use case is consuming the WoltLab/d.ts repository with a tsconfig.json that has strict: true
configured.
Within that repository there is an element with an incompatible overload in:
and it is registered in global.d.ts in:
https://github.com/WoltLab/d.ts/blob/57f923f03e37885885326bbd622d15b554feb9b5/global.d.ts#L122
Now when attempting to compile a project that has the repository as a dependency, the following error is reported:
node_modules/typescript/lib/lib.dom.d.ts:10535:87 - error TS2344: Type 'HTMLElementTagNameMap[K]' does not satisfy the constraint 'Node'.
Type 'HTMLInputElement | HTMLElement | WoltlabCoreDialogElement | HTMLDialogElement | WoltlabCoreDialogControlElement | ... 66 more ... | HTMLVideoElement' is not assignable to type 'Node'.
Type 'WoltlabCoreDialogElement' is not assignable to type 'Node'.
Types of property 'addEventListener' are incompatible.
Type '<T extends keyof WoltlabCoreDialogEventMap>(type: T, listener: (this: WoltlabCoreDialogElement, ev: WoltlabCoreDialogEventMap[T]) => any, options?: boolean | ... 1 more ... | undefined) => void' is not assignable to type '(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions | undefined) => void'.
Types of parameters 'listener' and 'callback' are incompatible.
Type 'EventListenerOrEventListenerObject | null' is not assignable to type '(this: WoltlabCoreDialogElement, ev: CustomEvent<any> | CustomEvent<ValidateCallback[]>) => any'.
Type 'null' is not assignable to type '(this: WoltlabCoreDialogElement, ev: CustomEvent<any> | CustomEvent<ValidateCallback[]>) => any'.
10535 querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;