Search code examples
javascripthtmlangulartypescriptcasting

Set the type of Node.childNodes variable in typescript


I have somewhere in my code:

const myElem: HTMLElement | null = document.getElementById("asd");
let arrow: any = [];

if (myElem) {
  arrow = myElem.childNodes;
  arrow[1].style.display = "none";
  arrow[2].style.display = "block";
}

I'm attempting to replace the any type of the arrow variable. It needs to be declared and initialized before it gets a value. I attempted:

let arrow: HTMLCollectionOf<HTMLElement> = [];

arrow = myElem.childNodes as HTMLCollectionOf<HTMLElement>;

Because official documentation says: NodeList objects are collections of nodes, usually returned by properties such as Node.childNodes and methods such as document.querySelectorAll().

and

The read-only childNodes property of the Node interface returns a live NodeList of child nodes of the given element where the first child node is assigned index 0. Child nodes include elements, text and comments.

nodelist doc

node childnode doc

The errors I get with what I attempted:

error TS2739: Type 'never[]' is missing the following properties from type 'HTMLCollectionOf<HTMLElement>': item, namedItem

error TS2352: Conversion of type 'NodeListOf<ChildNode>' to type 'HTMLCollectionOf<HTMLElement>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Property 'namedItem' is missing in type 'NodeListOf<ChildNode>' but required in type 'HTMLCollectionOf<HTMLElement>'.


Solution

  • If you have a particular reason to use childNodes (such as wanting non-element nodes), set the type of arrow to NodeListOf<ChildNode>. All your error messages are hinting you what to do. It's like they're saying "You said arrow would be a <X>, but then you assigned a value to it of type NodeListOf<ChildNode>, which is not like a <X>". If you go this route, you'll also have to do a type assertion / type check to use the style property, because that property is on the HTMLElement type- not on the ChildNode type.

    Now this leaves the talk about your initialization.

    If you're not going to use arrow again after the body of that if block, then just move the scope of arrow to go inside the if block. That way you don't even need to write the type annotation. TypeScript will infer it.

    const myElem = document.getElementById("asd");
    if (myElem) {
      const arrow = myElem.childNodes;
      (arrow[1] as HTMLElement).style.display = "none";
      (arrow[2] as HTMLElement).style.display = "block";
    }
    

    If you will use arrow again after the body of that if block, then you've better have the type reflect that it might not be initialized by the if block, and what you initialize it to, which would then be the value. I'd suggest you use null or undefined instead of an empty array. It's a bit more conventional. Unless you have some great reason to use an empty array as the "default" value. Whatever you do, make the typings work. Ex.

    const myElem = document.getElementById("asd");
    let arrow: NodeListOf<ChildNode> | undefined = undefined;
    if (myElem) {
      arrow = myElem.childNodes;
      (arrow[1] as HTMLElement).style.display = "none";
      (arrow[2] as HTMLElement).style.display = "block";
    }
    

    Notice that I took out your manual typing of myElem. It's redundant because it's identical to the return type of document.getElementById.


    If you don't have a particular reason to use childNodes (such as wanting non-element nodes), you could also use HTMLElement.children(), which returns a HTMLCollection, but you'll have to do a type assertion / type check to HTMLElement, since the type for elements of HTMLCollection is Element and not HTMLElement.