typescript for loop over HTMLCollection

Viewed 19085

I want to iterate over the results of

document.getElementsByTagName("...");

It returns an HTMLCollection, not an array. So I can't simply use forEach. The following is possible but doesn't look very nice:

let elements = document.getElementsByTagName("...");
for (var i = 0, m = elements.length; i < m; i++) {
    let element = elements[i];
}

For javascript, there exists pretty much the exact same question: For loop for HTMLCollection elements

And apparently, with the latest updates, modern browsers support:

var list = document.getElementsByClassName("events");
for (let item of list) {
    log(item.id);
}

But the typescript compiler complains:

error TS2495: Type 'NodeListOf<HTMLParagraphElement>' is not an array type or a string type.

It still transpiles to proper Javascript though. It's even aware of what I'm doing here and doesn't just copy over the sourcecode. The compiled output is this:

var elements = main_div.getElementsByTagName("p");
for (var _i = 0, elements_1 = elements; _i < elements_1.length; _i++) {
    var element = elements_1[_i];
}

This is great, since the generated code will be supported even on older browsers. But I would like to get rid of the error message.

My compilerOptions are this:

{
    "compilerOptions": {

        "module": "commonjs",
        "target": "es5",
        "sourceMap": true,
        "rootDir": "src",
        "lib": [
            "es2015",
            "dom"
        ]
    }
}

I did try to edit my lib options. This specific feature was a part of ES6 and has been reworked a few times. So I tested the various ecmascript versions in lib but couldn't find a working setup.

5 Answers

You can convert it to an array if you want to use forEach

const elements: Element[] = Array.from(document.getElementsByTagName("..."));
elements.forEach((el: Element) => {
    // do something
})

The typescript compiler supports this after specifying an extra flag, downlevelIteration:

{
    "compilerOptions": {
        "target": "es5",
        "downlevelIteration": true
    }
}

However, this will not only remove the error, it will also change the compiler output. This input typescript:

function compileTest(){
  let elements = document.getElementsByTagName("p");
  for(let element of elements){
    console.log(element);
  }
}

is compiled to this javascript:

function compileTest() {
    var e_1, _a;
    var elements = document.getElementsByTagName("p");
    try {
        for (var elements_1 = __values(elements), elements_1_1 = elements_1.next(); !elements_1_1.done; elements_1_1 = elements_1.next()) {
            var element = elements_1_1.value;
            console.log(element);
        }
    }
    catch (e_1_1) { e_1 = { error: e_1_1 }; }
    finally {
        try {
            if (elements_1_1 && !elements_1_1.done && (_a = elements_1.return)) _a.call(elements_1);
        }
        finally { if (e_1) throw e_1.error; }
    }
}

To get rid of the error message, you can add the list as any type. Typescript difference from JavaScript is the Type. Therefore, It's best practice to add the type to your variable. For your reference, I add the any type:

var list: any = document.getElementsByClassName("events");
for (let item of list) {
    console.log(item.id);
}

Actually, this is a typical "contextual typing". Contextual typing occurs when the type of an expression is implied by its location. For the code above to give the type error, the TypeScript type checker used the type of the document.getElementsByClassName("events") to infer the type of the ForLoop function. If this function expression were not in a contextually typed position, the list would have type any, and no error would have been issued.

If the contextually typed expression contains explicit type information, the contextual type is ignored.

Contextual typing applies in many cases. Common cases include arguments to function calls, right hand sides of assignments, type assertions, members of object and array literals, and return statements. The contextual type also acts as a candidate type in best common type.[From TypeScript Document]

Example usage:

const inputs = document.getElementsByTagName("input");
for (let index = 0; index < inputs.length; index++) {
  const input = inputs.item(index);
}

I know this is a very late answer to the question, but thought it might be nice to have in the thread... In an Angular project, I was looking for a way to use array destructuring (who doesn't just love array destructuring amiright?) of HTMLCollectionOf<T>, like shown in the example below:

const [zoomControl] = document.getElementsByClassName('leaflet-control-zoom');

But would get the error:

'Type 'HTMLCollectionOf' must have a 'Symbol.iterator' method that returns an iterator.ts(2488)'

Also, I've had scenarios where iterating over the collections would be nice, e.g.:

const elementsWithClassName = document.getElementsByClassName('class-name');
for(const a of elementsWithClassName) {
  console.log(a)
}

I found that adding a global-types-extensions.d.ts file with the following contents:

declare global {
  interface HTMLCollectionOf<T extends Element> {
    [Symbol.iterator](): Iterator<T>;
  }
}
export {};

... Fixed the error/warning I was getting from TS.

So now, I can happily go around array destructuing and using for...of on any HTMLCollectionOf .

Related