Home > Blockchain >  TypeScript complaining when using more than one selector
TypeScript complaining when using more than one selector

Time:12-25

I'm using the following function to search HTML elements by text:

function findElementsByText(text: string): HTMLElement[] {
  const selectors = 'button'
  // without this, when `text` is empty, all the elements on the page will match
  const pattern = new RegExp(text === '' ? '^$' : text)

  return Array.from(document.querySelectorAll(selectors)).filter(
    (element) => element.innerText.match(pattern)
  )
}

I don't want to only search buttons, so I added anchor tags:

  const selectors = 'a, button'

But, strangely, now I get this TypeScript error:

Type 'Element[]' is not assignable to type 'HTMLElement[]'. Type 'Element' is missing the following properties from type 'HTMLElement': accessKey, accessKeyLabel, autocapitalize, dir, and 116 more.

Why did adding one more selector cause this, and how to remove the error?

Live code:

Edit TypeScript selectors

CodePudding user response:

Your code did work, because querySelector has an overload which returns HTMLButtonElement list

function findElementsByText(text: string): HTMLElement[] {
  const selectors = 'button'
  // without this, when `text` is empty, all the elements on the page will match
  const pattern = new RegExp(text === '' ? '^$' : text)

  return Array.from(document.querySelectorAll(selectors)).filter(
    //                       ^?
    // (method) ParentNode.querySelectorAll<"button">(selectors: "button"): NodeListOf<HTMLButtonElement> ( 2 overloads)
    (element) => element.innerText.match(pattern)
  )
}

If your selector is not a single tag name, you need to use as HTMLElement[] or provide generic type

function findElementsByText2(text: string): HTMLElement[] {
  const selectors = 'button, a'
  // without this, when `text` is empty, all the elements on the page will match
  const pattern = new RegExp(text === '' ? '^$' : text)

  //                                          vvvvvvvvvvv
  return Array.from(document.querySelectorAll<HTMLElement>(selectors)).filter(
    (element) => element.innerText.match(pattern)
  )
}

CodePudding user response:

TypeScript is smart enough that it can recognise that the string "button" will give a NodeList of HTMLButtonElements elements (you can tell by hovering over querySelectorAll in the link you provided).

Here is the type definition for it:

querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;
querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;
querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;

"button" and "a" are listed in HTMLElementTagNameMap

interface HTMLElementTagNameMap {
    "a": HTMLAnchorElement;
    "abbr": HTMLElement;

… so the first definition matches and it returns NodeListOf<HTMLElementTagNameMap[K]>

Once you change the selector to something more complex, it doesn't match anything (because it compares the whole string, it can't parse it at this point) so it falls back to the last definition which returns NodeListOf<E> where E has just been defined as something that extends Element.

Since the elements might be from a non-HTML source, it can't be HTMLElement, hence the error.

The simple workaround is to use a generic type to override that:

querySelectorAll<HTMLElement>(selectors)
  • Related