To turn a NodeList, Set, arguments object, or string into a real array, use Array.from(x) or the spread operator [...x]:
const divs = Array.from(document.querySelectorAll('div')); // NodeList -> Array
const items = [...new Set([1, 2, 2, 3])]; // Set -> Array, [1,2,3]
const chars = [...'hello']; // String -> Array, ['h','e','l','l','o']Both give you a genuine array with map, filter, reduce, and the rest of the array methods attached. The two are interchangeable most of the time, but they are not equivalent, and the difference is worth knowing before you reach for one out of habit.
Why you need this at all
A NodeList from querySelectorAll, the arguments object inside a function, an HTMLCollection, a Set, a Map, a string: none of these is an Array. They look close enough that people try .map() on a NodeList and get TypeError: nodeList.map is not a function. They are either array-like (numeric indexes plus a length, but no array methods) or iterable (they work in for...of and yield values one at a time). To get the array methods, you have to convert.
The old way was Array.prototype.slice.call(arguments), borrowing slice from the array prototype and calling it on the array-like. It works, but it reads like a workaround, because it is one. Array.from and spread replaced it years ago and are Baseline across every browser and Node version you will meet today.
Array.from vs spread: the one real difference
// Both work on anything iterable:
Array.from(new Set([1, 2, 3])); // [1, 2, 3]
[...new Set([1, 2, 3])]; // [1, 2, 3]Where they diverge:
- Spread only works on iterables.
[...x]needsxto implement the iterator protocol (arrays, strings, Sets, Maps, NodeLists, generators). Spread a plain array-like object and you getTypeError: x is not iterable. Array.fromhandles both iterables and array-likes. It accepts anything with a numericlength, even an object that is not iterable at all:
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
Array.from(arrayLike); // ['a', 'b']
[...arrayLike]; // TypeError: arrayLike is not iterableSo if you are handed a length-bearing object that is not iterable (some older DOM collections, some library return values), Array.from is the one that works.
The map callback is why Array.from still earns its place
The bigger reason to keep Array.from in your toolbox: it takes a map function as a second argument, applied to every value as the array is built. Spread cannot do this. MDN documents the callback as receiving the element and its index, exactly like Array.prototype.map.
// Convert AND transform in one pass:
const lengths = Array.from(document.querySelectorAll('p'), p => p.textContent.length);That is one allocation instead of two. With spread you would write [...nodes].map(p => p.textContent.length), which builds an intermediate array and then maps it. For a NodeList of a few elements the difference is academic, but the single-pass form is cleaner and the intent is clearer.
The callback also unlocks the trick that has nothing to do with conversion at all. Pass an array-like with just a length, no values, and the callback fills in each slot by index:
Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]Array.from({length: 5}) on its own gives you [undefined, undefined, undefined, undefined, undefined]. Add the mapper and you have generated a sequence from nothing. This is the idiomatic way to build a range of numbers, and it is exactly the pattern in fill an array with a number sequence. Spread has no equivalent: [...{length: 5}] throws, because the object is not iterable.
Drop the arguments object in modern code
The classic use of slice.call was the arguments object: array-like, not an array, and the only way (pre-ES2015) to get the variadic args of a function as something you could iterate. You can still convert it:
function sum() {
return Array.from(arguments).reduce((a, b) => a + b, 0);
}But you should not have to. Rest parameters give you a real array directly, with no conversion step:
function sum(...args) {
return args.reduce((a, b) => a + b, 0);
}args is already an Array. No arguments, no Array.from, no slice.call. Rest params also work in arrow functions (which have no arguments binding at all) and read far better at the call site. Reach for arguments only when maintaining old code; write ...args in anything new.
Quick reference
| You have | Iterable? | [...x] | Array.from(x) | Notes |
|---|---|---|---|---|
| NodeList / array | yes | works | works | either is fine |
| Set / Map | yes | works | works | spread is terser |
| String | yes | works | works | splits into code points |
arguments | yes | works | works | prefer ...args instead |
{length: n} array-like | no | throws | works | Array.from only |
| Need to map while converting | - | no | works | Array.from(x, fn) |
The rule of thumb: reach for [...x] when you just want a quick copy of something iterable, because it is the shorter read. Reach for Array.from when the source is a non-iterable array-like, or when you want to transform values in the same pass with the second-argument callback. Both produce a fresh, shallow copy, so neither mutates the original. Once you have an array, removing duplicates and the rest of the standard transforms are all available.
See also
- JavaScript array methods, explained: the
map,filter, andreducetoolkit you unlock once a NodeList or Set is a real array. - Fill an array with values or a number sequence: the
Array.from({length: n}, (_, i) => ...)pattern as a range builder, plus thefillreference trap. - Remove duplicates from an array: the
[...new Set(arr)]one-liner and how to dedupe arrays of objects by a key.
Sources
Authoritative references this article was fact-checked against.
- Array.from() - MDN Web Docsdeveloper.mozilla.org
- Spread syntax (...) - MDN Web Docsdeveloper.mozilla.org
- The arguments object - MDN Web Docsdeveloper.mozilla.org





