Typescript,'NodeListOf<Element>' is not an array type or a string type
Asked Answered
L

7

44

Converting my JS to TS strict mode.

The following syntax looks fine to me but TS is complaining in the for loop on allSubMenus with:

[ts] Type 'NodeListOf<Element>' is not an array type or a string type.

What am I missing?

function subAct(target:Node){

  const allSubMenus : NodeListOf<Element> = document.querySelectorAll('.subMenuItems') 

  for (const sub of allSubMenus){
    sub.classList.remove('active')
  }  
}
Liam answered 7/8, 2018 at 9:58 Comment(0)
D
42

You need to set the target compiler option to es6 or higher for NodeListOf<T> to be iterable.

Dogbane answered 7/8, 2018 at 12:24 Comment(1)
You'd have to loop over the indices yourself, something like: for (let i = 0; i < allSubMenus.length; i++) { allSubMenus[i].classList.remove('active'); }Dogbane
P
41

According to your typescript target compiler, parsing error can be occured.

The for-of loop, introduced in the sixth edition of EcmaScript (ES6). Thus, old browsers' JS engine can not understand for-of loop syntax. https://hacks.mozilla.org/2015/04/es6-in-depth-iterators-and-the-for-of-loop/

To solve this issue,

if you support latest modern browsers(>=ES6) only

change your TS target

//tsconfig.json
{
  "compilerOptions": {
    "target": "es6" //above es6 like ES2015,ES2018 ...
  }
}

if you support old browsers(<=ES5)

I presume that you are using next environment.

//.tsconfig.json
{
  "compilerOptions": {
    "target": "es5"
  }
}
  1. To keep for-of loop, use as any syntax

Note: "as any" will cast collection(Objects) to array and this will affect some type features within "for" scope.

//.ts
const allSubMenus : NodeListOf<SpecifiedElement> = document.querySelectorAll('.subMenuItems') 
    
for (const sub of allSubMenus as any){ // then will pass compiler
  sub.classList.remove('active')
}  

The above TS script will be compiled to

//.js output
var allSubMenus = document.querySelectorAll('.subMenuItems');

for (var _a = 0, _b = forms; _a < _b.length; _a++) {
    var sub = _b[_a];
    sub.classList.remove('active');
}

https://stackblitz.com/edit/node-ywn1bq?file=main.js

  1. Use classic for-loop syntax
const allSubMenus : NodeListOf<SpecifiedElement> = document.querySelectorAll('.subMenuItems') 

for (let i = 0; i < allSubMenus.length; i++) { 
  allSubMenus[i].classList.remove('active'); 
}    

<Element>

In addition to the above, to avoid the following warning,

Property '<property name>' does not exist on type 'Element'

you may specify <Element> if you know element type and type define exists.

//for example,
NodeListOf<Element>  => NodeListOf<HTMLFormElement>

ECMAScript(ES) history

https://codeburst.io/javascript-wtf-is-es6-es8-es-2017-ecmascript-dca859e4821c

Pfosi answered 7/11, 2018 at 5:23 Comment(7)
I have been trying to google this fix for awhile. Your answer worked perfectly for me. Thanks.Mutant
Merits of the answer aside, to anyone who reads this: please, don't use that!). A cast to any shuts down type checking - you lose all type safety for sub as well as lose the autocompletion benefits of the supporting IDEs (unless you cast back). Also of note: querySelectorAll retuns NodeListOf<Element> by default, no need to explicitly type the allSubMenus variable.Abbatial
The whole point of the the casting is so you dont just blatantly use any.. It's much better to change the target compiler to properly type your code so you don't run into "any" issues later on.Stonecutter
@RyanDennler , please check "Note" section. If a user used relatively latest TS compiler, the user would not end up with this topic. Development environtment can be vary and chaning TS target might not be a option for someone.Pfosi
@MaciejKrawczyk , would you be able to share what makes you think "This will make older browsers choke"?Pfosi
@Pfosi My apologies, I got this wrong. I will delete my previous comment.Odontoblast
No worries @MaciejKrawczyk , it is good to review old thread again.Pfosi
I
8

You could try

const allSubMenus : NodeListOf<Element> = document.querySelectorAll('.subMenuItems') 
Array.from(allSubMenus, subMenu => {/* */})
Ishii answered 10/10, 2019 at 10:4 Comment(1)
Or spread operator [...allSubMenus]Truncation
T
2

Set "downlevelIteration": true in compilerOptions in your tsconfig.json file.

From https://www.typescriptlang.org/tsconfig#downlevelIteration

Downleveling is TypeScript’s term for transpiling to an older version of JavaScript. This flag is to enable support for a more accurate implementation of how modern JavaScript iterates through new concepts in older JavaScript runtimes

Tine answered 28/1, 2021 at 8:6 Comment(0)
N
1

You can iterate over a NodeListOf with the forEach method.

const allSubMenus : NodeListOf<Element> = document.querySelectorAll('.subMenuItems') 
allSubMenus.forEach(sub => sub.classList.remove('active'))

Source: https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules_typedoc_node_modules_typescript_lib_lib_dom_d_.nodelistof.html#foreach

Newport answered 24/5, 2020 at 21:57 Comment(1)
at the time of writing, yes this is possible, however forEach has no break, so it's not always a full replacement...Sfax
B
1

while compiling your code you may choose target: ES2017 or target: ES2015. also you can set this property in tsconfig.json file unser compilerOptions.

So here is the command line code for that:

npx tsc path/to/file.ts --target ES2015

TIP: if you are using babel along side with typescript, it is totally recommended that you always make typescript to compile to latest version of Javascript, and then let babel handles rest of transpiring process. With this technique you add another lever of assurance of supportive level to older browsers since typescript is not ok to compile the code that would run in for example ie6; so babel comes to rescue here and make you yo make sure that your js code would run in even ie < 9 with it's helpful polyfills and other mechanisms that it takes to work!

So keep in mind that:

Always let typescript compiles your code to latest javascript ( by setting target: ES2017 ) and let babel transpiles your js code to support older browsers ( separate concerns properly and let each one does the related job).

Balbinder answered 21/3, 2021 at 12:5 Comment(0)
O
1

This assumes you want to keep ES5 target (if not, upgrading it to ES6 will solve the issue as well).

  1. Use for loop
function subAct(target:Node){
  const allSubMenus = document.querySelectorAll('.subMenuItems');

  for (let i = 0; i < allSubMenus.length; i += 1){
    const sub = allSubMenus[i];
    sub.classList.remove('active')
  }  
}
  1. Use Array.from

In order to use this, you have to add ES2015.core to compilerOptions.lib and add polyfill for Array.prototype.from Note that this will loop on the collection twice - the first method is better.

function subAct(target:Node){
  const allSubMenus = Array.from(document.querySelectorAll('.subMenuItems'));

  for (const sub of allSubMenus){
    sub.classList.remove('active')
  }  
}
Odontoblast answered 2/8, 2022 at 8:27 Comment(2)
If you use "Array.from", you need to change either your compile target or the 'lib' compiler option to 'es2015' or later, right?Pfosi
@Pfosi Correct, add ES2015.core to the lib and import array polyfills (i.e. from core-js). I will update my answer.Odontoblast

© 2022 - 2024 — McMap. All rights reserved.