Zip Multiple Arrays
Python users miss `zip(a, b)` and the Lodash crowd reaches for `_.zip`, but JavaScript can do this cleanly with one helper. This snippet covers the basic two-array zip, an N-array variadic version, the unzip inverse, and a `zipWith` that lets you fold pairs into custom shapes (records, objects, weighted sums). It also clarifies the truncate-to-shortest vs. fill-with-undefined trade-off.
1,003 views
6
function zip2(a, b) {
const len = Math.min(a.length, b.length);
const out = new Array(len);
for (let i = 0; i < len; i++) {
out[i] = [a[i], b[i]];
}
return out;
}
const names = ['Ada', 'Bo', 'Cal'];
const ages = [30, 25, 40];
console.log(zip2(names, ages));
// [['Ada', 30], ['Bo', 25], ['Cal', 40]]
// Truncate-to-shortest is the safe default.
console.log(zip2(['a', 'b', 'c', 'd'], [1, 2]));
// [['a', 1], ['b', 2]]The simplest zip walks both arrays in lockstep and emits [a[i], b[i]] pairs until the shorter input runs out. Pre-allocating with new Array(len) skips the array growth dance V8 does when you push repeatedly, which matters on hot paths. Truncating to the shortest length is the safer default: if a downstream map callback expects a defined element from each side, padding with undefined would only push the bug one step away. The whole pass is O(n) time and O(n) output.
function zip(...arrays) {
if (arrays.length === 0) return [];
const len = Math.min(...arrays.map((a) => a.length));
const out = new Array(len);
for (let i = 0; i < len; i++) {
const tuple = new Array(arrays.length);
for (let j = 0; j < arrays.length; j++) {
tuple[j] = arrays[j][i];
}
out[i] = tuple;
}
return out;
}
const usernames = ['ada', 'bo', 'cal'];
const emails = ['[email protected]', '[email protected]', '[email protected]'];
const scores = [98, 80, 91];
console.log(zip(usernames, emails, scores));
// [['ada', '[email protected]', 98], ['bo', '[email protected]', 80], ['cal', '[email protected]', 91]]
console.log(zip()); // []
console.log(zip([1, 2, 3])); // [[1], [2], [3]]Real columns rarely come in pairs; you usually have three or four parallel arrays (id, name, status, total) and want them zipped into row tuples. The variadic form takes any number of inputs via rest spread and computes the common length once. The inner loop fills a fixed-size tuple for each row, so the algorithm stays O(rows * cols) with no hidden allocations. Edge cases worth handling explicitly: zero arrays returns [], one array returns single-element tuples (useful as a noop adapter to a downstream API that expects tuples).
function unzip(rows) {
if (rows.length === 0) return [];
const cols = rows[0].length;
const out = Array.from({ length: cols }, () => []);
for (const row of rows) {
for (let j = 0; j < cols; j++) {
out[j].push(row[j]);
}
}
return out;
}
const pairs = [
['Ada', 30],
['Bo', 25],
['Cal', 40],
];
const [peopleNames, peopleAges] = unzip(pairs);
console.log(peopleNames); // ['Ada', 'Bo', 'Cal']
console.log(peopleAges); // [30, 25, 40]
// Round-trip with zip is identity for rectangular input.
const cols = unzip(pairs);
const back = pairs.map((_, i) => cols.map((c) => c[i]));
console.log(JSON.stringify(back) === JSON.stringify(pairs)); // trueUnzipping is the inverse: take rows like [[name, age], ...] and produce columns like [names, ages]. The implementation reads the column count from the first row and fills a per-column array as it walks the rows. This is exactly what you want when an API gives you rows but you need to feed a charting library that takes parallel column arrays. Note that the helper assumes a rectangular input; if rows have different lengths, the missing cells will be undefined in the columns, which is usually a sign of upstream data corruption rather than something to silently paper over.
function zipWith(arrays, combine) {
if (arrays.length === 0) return [];
const len = Math.min(...arrays.map((a) => a.length));
const out = new Array(len);
for (let i = 0; i < len; i++) {
out[i] = combine(...arrays.map((a) => a[i]));
}
return out;
}
// 1. Build records by combining columns into objects.
const ids = [1, 2, 3];
const names2 = ['Ada', 'Bo', 'Cal'];
const records = zipWith([ids, names2], (id, name) => ({ id, name }));
console.log(records);
// [{ id: 1, name: 'Ada' }, { id: 2, name: 'Bo' }, { id: 3, name: 'Cal' }]
// 2. Weighted sum across three vectors.
const xs = [1, 2, 3];
const ys = [10, 20, 30];
const ws = [0.5, 0.5, 0.5];
const blended = zipWith([xs, ys, ws], (x, y, w) => w * x + (1 - w) * y);
console.log(blended); // [5.5, 11, 16.5]Most of the time you do not actually want raw tuples; you want some derived shape. zipWith accepts a combiner function and applies it to each parallel slice, so you can build records, weighted blends, or anything else in one pass without an extra .map. Callers who do want raw tuples can always pass (...args) => args and get plain zip behaviour back. This single helper subsumes most of what the Lodash family zip, zipObject, zipWith, and mergeWith do for parallel inputs, and it costs about ten lines of code.
