When working with TypeScript, it's interesting to note why certain operations are allowed while others are not.
The reason behind this distinction lies in the fundamental difference between querySelector
and Event["target"]
. While querySelector
is an overloaded generic function that can handle complex type mappings, Event["target"]
has a simpler type structure. In essence, you cannot assign a superclass like EventTarget
or Element
to a subclass like HTMLInputElement
without narrowing the type using assertions or predicates. The intricacies of the return type of
querySelector</code play a crucial role in preventing such mismatches.</p>
<p>However, when you specifically use the string literal <code>"input"
as the argument for
querySelector
, a specific signature comes into play:
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
This allows for precise mapping from the string literal "input"
to
HTMLInputElement</code without requiring explicit type annotations.</p>
<p>On the other hand, <code>Event["target"]
simply defines:
readonly target: EventTarget | null;
Due to its straightforward definition, treating it as a subtype instance necessitates either type assertions or predicates.
An intriguing scenario arises when we consider the following example:
function example(selector: string) {
const element1 = document.querySelector(selector);
// ^? const element1: Element | null
const element2: HTMLInputElement | null = document.querySelector(selector);
// ^? const element2: HTMLInputElement | null
}
In this case, the previously mentioned overload does not apply because the argument is a general string rather than a specific string literal such as "input." This leads us to the following overload:
querySelector<E extends Element = Element>(selectors: string): E | null;
For element1
, where no type argument is provided, TypeScript defaults to Element
, resulting in a return type of Element | null.
Surprisingly, for element2
, although no type assertion or predicate is used, TypeScript infers the type argument from element2
's declared type. This inference process allows for the assignment of Element | null
to HTMLInputElement | null
, making the assignment valid by interpreting the specifics of each variable's type.
(All examples are demonstrated with strict checks enabled, overlooking the presence of null values.😊)