skip to content

Given the same argument x, what can Array.from(x) do that the array spread [...x] cannot?

level: middleimportance: should knowfreq 45%

answer

  1. one accepts a larger input set
  2. length plus numeric keys
  3. a second argument you cannot spread
  4. { length: 5 } builds a range
  5. empty array instead of a throw

basics

~10 s

Array.from also accepts array-likes — objects with a length and indexed properties but no Symbol.iterator — and takes an optional mapping function as its second argument. Array spread only accepts iterables and cannot map.

solid answer

~50 s

Two things. First, `Array.from` handles *array-likes* as well as iterables: if the argument has no `Symbol.iterator`, it falls back to reading `length` and the properties `0`, `1`, and so on, so `Array.from({ length: 3 })` yields `[undefined, undefined, undefined]` where `[...{ length: 3 }]` throws a `TypeError`. Second, it takes an optional mapping function as a second argument, called with `(element, index)`, which is why `Array.from({ length: 5 }, (_, i) => i)` is the idiomatic way to build a range — spread has no equivalent and would need a separate `.map()` pass over an intermediate array. For a genuine iterable such as a `Set`, a `Map` or a string, `[...x]` and `Array.from(x)` produce the same array, and spread reads more cleanly. Reach for `Array.from` when the source is array-like or when you want the map built in.

code

javascript · 11 lines
javascript
const arrayLike = { length: 2, 0: 'a', 1: 'b' };

console.log(Array.from(arrayLike));              // ['a', 'b']
console.log(Array.from({ length: 5 }, (_, i) => i)); // [0, 1, 2, 3, 4]
console.log(Array.from({ a: 1 }));               // [] — silent, not a throw

try {
  console.log([...arrayLike]);
} catch (e) {
  console.log(e instanceof TypeError);           // true
}

go deeper

for a junior

Know both convert something into a real array, and that Array.from takes an optional second argument used to map each element as it is produced.

for a middle

Explain the input contracts: spread needs Symbol.iterator, Array.from falls back to length plus indexed properties. Be able to show why Array.from({ length: n }, fn) builds a range that map on a sparse array cannot.

for a senior

Recognise the silent-empty-array failure when Array.from meets a record, and choose the conversion that fails loudly at boundaries. Know that argument spread of a huge sequence can hit the engine's argument limit.

for a principal

Decide where in a system conversions from foreign sequence-shaped values happen at all. Converting once at the boundary and passing arrays inward is usually cheaper to reason about than threading iterables through every layer.

## Two conversions with different input contracts `[...x]` is defined purely in terms of the iteration protocol: get `x[Symbol.iterator]()`, pump `next()` until `done`, collect the values. If `x` has no `Symbol.iterator`, it throws a `TypeError` immediately. `Array.from(x)` accepts a strictly larger set of inputs. Its algorithm checks for `Symbol.iterator` first and, when present, iterates exactly as spread would. When it is absent, instead of failing it treats `x` as an *array-like*: it reads `x.length`, converts it to a non-negative integer, and reads the properties `"0"` through `"length - 1"`, substituting `undefined` for any that are missing. ```js const arrayLike = { length: 2, 0: 'a', 1: 'b' }; Array.from(arrayLike); // ['a', 'b'] [...arrayLike]; // TypeError: arrayLike is not iterable ``` The classic array-likes are objects returned from older APIs and anything you build by hand with a numeric shape. Note that `arguments` is *both* — it has a `Symbol.iterator`, so spread works on it too. ## The mapping function `Array.from(source, mapFn, thisArg)` calls `mapFn(element, index)` for each produced element and stores the result. It is not merely sugar for `.map()`: the mapping happens while the array is being built, so no intermediate array is created, and — importantly — it applies to positions the array-like never actually defined. ```js Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4] Array.from({ length: 3 }, () => []); // three distinct arrays ``` That second line is the standard way to create a grid of independent rows. Attempting the same with `new Array(3).fill([])` gives three references to *one* array, a bug that surfaces the moment you push into a row. The difference also shows on sparse arrays. `new Array(3)` has holes, and `.map()` skips holes, so `new Array(3).map((_, i) => i)` still returns a hole-filled array. `Array.from({ length: 3 }, (_, i) => i)` visits every index because `Array.from` reads each index explicitly rather than iterating existing elements. ## Where they agree For any genuine iterable, both produce the same array: ```js const s = new Set([1, 2, 2, 3]); [...s]; // [1, 2, 3] Array.from(s); // [1, 2, 3] [...'hi']; // ['h', 'i'] Array.from('hi'); // ['h', 'i'] const m = new Map([['a', 1]]); Array.from(m); // [['a', 1]] ``` Both also drain the iterator exactly once and to completion, so both leave a single-use iterator exhausted. ## Choosing between them In real code the guidance is simple: - Source is an iterable and you want a plain copy: use `[...x]`. It is shorter, and it composes inside a larger literal — `[first, ...rest, last]` has no `Array.from` equivalent. - Source is array-like, or has no `Symbol.iterator` and you cannot change it: use `Array.from(x)`. - You want the elements transformed as you convert, or you are synthesising a sequence from a bare `length`: use `Array.from(x, mapFn)`. One further practical difference: spread expands into an argument list in the `f(...x)` form, and very large expansions can exceed the engine's argument limit and throw a `RangeError`. `Array.from` builds an array without going through an argument list, so it is the safer conversion for sequences that might be enormous. That limit applies to argument spread specifically, not to `[...x]` in an array literal. ## The pitfall to name in an interview The single most common mistake is assuming `Array.from` silently rescues *anything*. It does not rescue a plain object with named keys: `Array.from({ a: 1, b: 2 })` returns `[]`, because the object has no `Symbol.iterator` and no `length`, so the length reads as `undefined`, converts to `0`, and produces an empty array. Returning an empty array rather than throwing makes this failure quieter — and therefore more dangerous — than the `TypeError` spread would have given you. When the source is a record, `Object.values` or `Object.entries` is what you actually want.

  • What does Array.from({ a: 1, b: 2 }) return, and why is that dangerous?
    It returns `[]`. The object has no `Symbol.iterator`, so `Array.from` falls back to the array-like path, reads `length` as `undefined`, converts it to `0`, and builds an empty array. Unlike spread, which throws a clear TypeError, this failure is silent — a good reason to use `Object.values` on records.
  • Why is Array.from({ length: 3 }, () => []) preferred over new Array(3).fill([])?
    `fill` stores the *same* array reference in all three slots, so pushing into one row appears in all of them. `Array.from` calls the mapping function once per index, producing three independent arrays. The distinction matters for any mutable default value.
  • Is Array.from(x, fn) different from [...x].map(fn) beyond style?
    Yes in two ways. It avoids allocating the intermediate array, and it visits every index of an array-like — including positions that were never defined — whereas `.map()` skips holes in a sparse array. For a dense iterable source the results match.

saying these in an interview costs you the question

  • Says Array.from works on any plain object
  • Thinks spread accepts array-likes with a length
  • Believes Array.from(x, fn) just calls map afterwards
  • Claims new Array(3).map fills the indexes
  • Uses fill with an object literal for independent rows

context