Define 'tagName' values in Element interfaces to prevent misleading type acceptance.
Bug Report
🔎 Search Terms
- HTML Element Interfaces
- Element tagName
- Element Interfaces
- Incorrect element types accepted
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about Element interfaces
⏯ Playground Link
Playground link with relevant code
💻 Code
// Elements can be assigned to variables of other element types because of interface overlaps being sufficient.
let element: HTMLSpanElement = document.createElement("div");
// If the interfaces included the appropriate 'tagName' property this could be avoided...
let spanElement: HTMLSpanElement & { tagName: "SPAN" } = document.createElement("div") as HTMLDivElement & { tagName: "DIV" };
🙁 Actual behavior
e.g. 'HTMLDivElement' instance can be assigned to variables of type 'HTMLSpanElement'.
🙂 Expected behavior
This should fail the type checking to avoid accidentally assigning elements of an incorrect type.
I wonder if it would make some methods invariant (as it was with cloneNode) and will be a breaking change in types?
However you can extend existing interfaces in the meantime by creating e.g. strict-tagname.d.ts with content like
interface HTMLSpanElement {
tagName: "SPAN"
}
interface HTMLDivElement {
tagName: "DIV"
}
interface HTMLParagraphElement {
tagName: "P"
}
You can do something like (This is updated answer btw as I bumped into issue discussed below):
export type NominalHTMLDivElement = HTMLDivElement & { readonly '': unique symbol };
export type NominalHTMLSpanElement = HTMLSpanElement & { readonly '': unique symbol };
var div = document.createElement("div") as NominalHTMLDivElement;
var span = document.createElement("span") as NominalHTMLSpanElement;
div = span; // Compile time error
span = div; // Compile time error
Be careful though as you are not allowed to wrap nominal type declaration into a separate type as in such case a unique symbol will be shared across all type usages which would essentially share type "nominality" and thus beat it's original purpose.
export type Nominal<T> = T & { readonly '': unique symbol };
export type NominalHTMLDivElement = Nominal<HTMLDivElement>;
export type NominalHTMLSpanElement = Nominal<HTMLSpanElement>;
var div = document.createElement("div") as NominalHTMLDivElement;
var span = document.createElement("span") as NominalHTMLSpanElement;
span = div; // Does not give error! :(
The suggested solution is still far from being perfect but covered most of my use-cases I had chance to face
https://github.com/Microsoft/TypeScript/issues/202#issuecomment-961853101
This was quite confusing. tagName of all element interfaces is string, even though its value is a constant. Each element should override the root HTMLElement.tagName with its constant value, e.g. interface HTMLDivElement extends HTMLElement { readonly tagName: 'DIV', … }