TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Define 'tagName' values in Element interfaces to prevent misleading type acceptance.

Open Joewsh opened this issue 4 years ago • 3 comments

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.

Joewsh avatar Jul 28 '21 16:07 Joewsh

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"
}

Playground

IllusionMH avatar Jul 28 '21 17:07 IllusionMH

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

See code

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! :(

See code

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

lu4 avatar Nov 05 '21 12:11 lu4

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', … }

JakobJingleheimer avatar Mar 09 '25 11:03 JakobJingleheimer