24

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.

lhk
  • 27,458
  • 30
  • 122
  • 201

5 Answers5

13

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
})
Bill
  • 469
  • 4
  • 7
4

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; }
    }
}
lhk
  • 27,458
  • 30
  • 122
  • 201
  • Setting the target to es5 just so that "for of" loop works with HTMLCollection object is not ideal. Just use a for loop. Set to es5 only if you want to support old browsers like IE. Use ES2015. Smaller bundle size. – DanKodi Jun 09 '20 at 02:16
  • You got that wrong, the necessary compiler setting is downleveliteration. It has nothing to do with es5 (the javascript version was a constraint by the project). In fact, if anything it’s impressive that tsc is able to emit compatible syntax all the way back to es5. – lhk Jun 09 '20 at 06:12
3

Example usage:

const inputs = document.getElementsByTagName("input");
for (let index = 0; index < inputs.length; index++) {
  const input = inputs.item(index);
}
DanKodi
  • 3,550
  • 27
  • 26
  • 2
    did you even read the question? This is almost exactly the same as the second code sample. – lhk Jun 01 '20 at 09:04
  • almost!. My answer uses the function item(index) exposed by the HTMLCollection (https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection/item). – DanKodi Jun 02 '20 at 03:38
  • @DanKodi: OP knows that this solution works, but doesn't think that it looks very nice. Adding .item probably doesn't change that feeling. :) – Jan Aagaard Feb 10 '21 at 10:06
1

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]

Ortsbo
  • 175
  • 1
  • 7
1

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 .

Batuik
  • 11
  • 2
  • Eh, did you even look at the accepted answer? This question is getting flooded with weird workarounds while there exists a very clean solution. Just specify it in the compiler flags. – lhk Jun 04 '20 at 05:16
  • I did look at the accepted answer and tried implementing the suggested changes to the `.tsconfig` file; however, this did not solve my issue. Perhaps, this is because Angular by default now targets `es2015`. – Batuik Jun 04 '20 at 08:06
  • the declaration that you've added should be pretty much the same that will be added by a correctly configured tsconfig. Are you sure that you've done that properly? For example, you're writing `.tsconfig`, is this actually the name that you use? It should be `tsconfig.json` – lhk Jun 04 '20 at 09:04
  • You're right! I was referring to the `tsconfig.json` file, and simply made a mistake when referring to this file as "`.tsconfig`" in my previous comment, which it seems I am now unable to edit. Based on your answer I went looking for possible reasons why the suggested solution didn't seem to work for me; I found that adding `DOM.Iterable` to the `lib` array made the difference for me. Turns out, when targeting `ES2015`, you don't even need to specify `"downlevelIteration": true` in `compilerOptions`. Thanks for the tip regarding a _correctly_ configured tsconfig! – Batuik Jun 04 '20 at 23:19