How to pair, combine, transpose, and stream arrays — without Lodash — using clean, reusable utilities (with TypeScript, iterables, and async support).

Introduction
You’ve got two arrays — keys and values, headers and rows, labels and inputs — and you need to pair items by position. That’s “zipping.” Think of it like a jacket zipper: tooth-to-tooth alignment across two tracks.
Most devs reach for Lodash’s zip
, zipObject
, or unzip
. You don’t need to. Modern JavaScript makes zipping tiny, readable, and fast—with a few edge-case traps (uneven lengths, sparse arrays, async streams) you should handle deliberately.
In this guide, you’ll get production-ready zip utilities, TypeScript typings, lazy generator versions, zipLongest, zipWith, transpose, and unzip — plus practical use cases in React, Node, and data pipelines.
1) What Is “Zip”? (And Why It’s Useful)
Zip = combine multiple arrays by index:
['a','b','c']
and[1,2,3]
→[ ['a',1], ['b',2], ['c',3] ]
.
Where you’ll use it
- Build objects from separate
keys
andvalues
. - Render tables: headers ↔ cells.
- CSV/JSON transforms: columns ↔ row values.
- i18n merging: source keys ↔ translated strings.
- Pairwise processing: current and next element (diffs, line segments).
- Matrix ops: transpose (rows ↔ columns).
2) A Minimal zip
(Two Arrays)
function zip(a, b) {
const len = Math.min(a.length, b.length);
const out = Array(len);
for (let i = 0; i < len; i++) out[i] = [a[i], b[i]];
return out;
}
zip(['a','b','c'], [1,2,3]);
// [['a',1], ['b',2], ['c',3]]
Why this design:
- Uses
Math.min
to avoid undefined pairs. - Preallocates result for speed.
- Clear and predictable.
Gotcha: Uneven arrays truncate silently. If you need to keep extras, see zipLongest
.
3) zip
for N Arrays (Variadic)
function zipN(...arrays) {
const len = Math.min(...arrays.map(a => a.length));
const out = Array(len);
for (let i = 0; i < len; i++) {
out[i] = arrays.map(a => a[i]);
}
return out;
}
// zipN(a, b, c) -> [[a0,b0,c0], [a1,b1,c1], ...]
When to use: More than two arrays (e.g., id
, name
, email
).
Behavior: Truncates to the shortest length.
4) zipLongest
(Pad to the Longest)
Sometimes you must keep all elements and pad missing spots (like Python’s itertools.zip_longest
).
function zipLongest(pad, ...arrays) {
const len = Math.max(...arrays.map(a => a.length));
const out = Array(len);
for (let i = 0; i < len; i++) {
out[i] = arrays.map(a => (i < a.length ? a[i] : pad));
}
return out;
}
zipLongest(null, ['a','b'], [1,2,3,4]);
// [['a',1], ['b',2], [null,3], [null,4]]
Tip: Use meaningful pads: null
, undefined
, ''
, or a sentinel object.
5) zipWith
(Transform While Zipping)
Compute on the fly: “zip and immediately map”. Great for building objects, sums, concatenations, etc.
function zipWith(fn, ...arrays) {
const len = Math.min(...arrays.map(a => a.length));
const out = Array(len);
for (let i = 0; i < len; i++) {
const args = arrays.map(a => a[i]);
out[i] = fn(...args, i);
}
return out;
}
// Example: sum positions
zipWith((x, y) => x + y, [1,2,3], [10,20,30]); // [11,22,33]
6) zipObject
(Keys + Values → Object)
function zipObject(keys, values) {
const len = Math.min(keys.length, values.length);
const out = {};
for (let i = 0; i < len; i++) out[keys[i]] = values[i];
return out;
}
zipObject(['id','name'], [42,'Rahmat']);
// { id: 42, name: 'Rahmat' }
Variations
- Ignore extras (default above).
- Error on mismatch if you want strictness:
function zipObjectStrict(keys, values) {
if (keys.length !== values.length) {
throw new Error('zipObjectStrict: keys and values length mismatch');
}
return zipObject(keys, values);
}
7) unzip
(Inverse of Zip) & transpose
(Matrix Rows ↔ Columns)
function unzip(pairs) {
const a = [], b = [];
for (const [x, y] of pairs) { a.push(x); b.push(y); }
return [a, b];
}
unzip([['a',1], ['b',2]]);
// [['a','b'], [1,2]]
Transpose generalizes that idea for N arrays (think 2D matrix):
function transpose(matrix) {
const rows = matrix.length;
const cols = rows ? Math.max(...matrix.map(r => r.length)) : 0;
const out = Array.from({ length: cols }, () => Array(rows));
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
out[c][r] = matrix[r][c];
}
}
return out;
}
// transpose([[1,2,3],[4,5,6]]) -> [[1,4],[2,5],[3,6]]
Use cases:
- Rotating tables (columns ↔ rows).
- Aggregating series by index.
- Linear algebra / ML preprocessing.
8) Lazy Zipping with Iterables (No Big Arrays Needed)
For streams or large datasets, don’t allocate everything up front. Use generators.
zipIter
(truncates to shortest)
function* zipIter(...iters) {
const iterators = iters.map(it => it[Symbol.iterator]());
while (true) {
const nexts = iterators.map(it => it.next());
if (nexts.some(n => n.done)) return;
yield nexts.map(n => n.value);
}
}
zipLongestIter
function* zipLongestIter(pad, ...iters) {
const itersArr = iters.map(it => it[Symbol.iterator]());
while (itersArr.length) {
const nexts = itersArr.map(it => it.next());
if (nexts.every(n => n.done)) return;
yield nexts.map(n => (n.done ? pad : n.value));
}
}
When to use:
- Log processing, file reads, pagination streams.
- Pairing live event sources (e.g., RxJS alternatives with bare iterables).
9) Async Zipping (API Pagination, I/O Pipelines)
Sometimes your sources are async iterables (e.g., for await...of
).
async function* zipAsyncIter(...asyncIters) {
const iterators = asyncIters.map(it => it[Symbol.asyncIterator]());
while (true) {
const nexts = await Promise.all(iterators.map(it => it.next()));
if (nexts.some(n => n.done)) return;
yield nexts.map(n => n.value);
}
}
// usage
// for await (const [rowA, rowB] of zipAsyncIter(streamA(), streamB())) { ... }
Caution: This awaits all sources each step; if one is slow, the whole step waits. For race-like behavior or timeouts, add cancellation/timeout logic (AbortController, Promise.race
, etc.).
10) Real-World Patterns You’ll Reuse
A) Build a lookup map from two arrays
const ids = [101, 102, 103];
const names = ['Alpha', 'Beta', 'Gamma'];
const map = new Map(zip(ids, names)); // Map(101->'Alpha', ...)
B) Render a table (React)
function Table({ headers, rows }) {
return (
<table>
<thead>
<tr>{headers.map(h => <th key={h}>{h}</th>)}</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{zip(headers, row).map(([h, cell]) => (
<td key={h}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Why zip here? Aligns each cell with its header by position — clean and safe against misalignment bugs.
C) Diff two arrays pairwise (adjacent pairs)
function pairwise(arr) {
const out = [];
for (let i = 0; i < arr.length - 1; i++) out.push([arr[i], arr[i+1]]);
return out;
}
pairwise([10, 13, 12, 15]);
// [[10,13],[13,12],[12,15]]
D) CSV row → object by headers
const headers = ['id','name','email'];
const row = ['42','Rahmat','r@example.com'];
const obj = zipObject(headers, row);
// { id:'42', name:'Rahmat', email:'r@example.com' }
E) Merge i18n keys and translations
const keys = ['app.title', 'app.ok', 'app.cancel'];
const ur = ['عنوان', 'ٹھیک ہے', 'منسوخ'];
const dict = zipObject(keys, ur);
11) TypeScript Versions You Can Drop In
Basic zip
(tuple inference for two arrays)
export function zip<A, B>(a: A[], b: B[]): [A, B][] {
const len = Math.min(a.length, b.length);
const out: [A, B][] = new Array(len);
for (let i = 0; i < len; i++) out[i] = [a[i], b[i]];
return out;
}
Variadic zipN
(loosely typed, practical)
export function zipN<T extends unknown[][]>(...arrays: T): Array<{ [K in keyof T]: T[K] extends (infer U)[] ? U : never }> {
const len = Math.min(...arrays.map(a => a.length));
const out = new Array(len) as Array<{ [K in keyof T]: T[K] extends (infer U)[] ? U : never }>;
for (let i = 0; i < len; i++) {
out[i] = arrays.map(a => a[i]) as any;
}
return out;
}
zipWith
export function zipWith<Args extends unknown[], R>(
fn: (...args: [...Args, number]) => R,
...arrays: { [K in keyof Args]: Args[K][] }
): R[] {
const len = Math.min(...(arrays as unknown as unknown[][]).map(a => a.length));
const out: R[] = new Array(len);
for (let i = 0; i < len; i++) {
const args = arrays.map(a => a[i]) as Args;
out[i] = fn(...args, i);
}
return out;
}
zipObject
export function zipObject<K extends string | number | symbol, V>(keys: K[], values: V[]): Record<K, V> {
const len = Math.min(keys.length, values.length);
const out = {} as Record<K, V>;
for (let i = 0; i < len; i++) out[keys[i]] = values[i];
return out;
}
transpose
export function transpose<T>(matrix: T[][]): T[][] {
const rows = matrix.length;
const cols = rows ? Math.max(...matrix.map(r => r.length)) : 0;
const out: T[][] = Array.from({ length: cols }, () => Array(rows) as T[]);
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out[c][r] = matrix[r][c];
return out;
}
12) Edge Cases & Gotchas (Read This Twice)
- Uneven lengths: Decide upfront — truncate (
zip
), pad (zipLongest
), or throw (strict variants). - Sparse arrays (“holes”): Zipping preserves holes; downstream ops (
map
,forEach
) skip holes. ConsiderArray.from(array)
to densify (holes →undefined
). - Undefined vs. missing: If you use
zipLongest
withundefined
pad, you may confuse “value exists but undefined” with “no value.” Prefernull
or a sentinel. - Mutability: Zipped arrays hold references. Mutating the underlying arrays mutates what you see later.
- Big arrays: Preallocate outputs and avoid building huge intermediate tuples if memory matters.
- Async iterables: Zip waits for all sources each cycle. Add timeouts/backpressure if sources are imbalanced.
13) Performance Notes (Without Micro-Bench Theater)
- Loops beat clever chains for large data. The simple
for
versions above are plenty fast. - Preallocation (
new Array(len)
) reduces GC pressure vspush
. - Generators avoid building the entire result in memory — perfect for streaming transformations.
zipWith
can fuse operations (zip + map) in a single pass.
When in doubt, write the clearest solution first, then benchmark hot spots.
14) Practical “Batteries Included” Helpers (Copy–Paste)
// 1) Pair two arrays (truncate)
export function zip(a, b) {
const len = Math.min(a.length, b.length);
const out = Array(len);
for (let i = 0; i < len; i++) out[i] = [a[i], b[i]];
return out;
}
// 2) Zip N arrays (truncate)
export function zipN(...arrays) {
const len = Math.min(...arrays.map(a => a.length));
const out = Array(len);
for (let i = 0; i < len; i++) out[i] = arrays.map(a => a[i]);
return out;
}
// 3) Zip-with transform
export function zipWith(fn, ...arrays) {
const len = Math.min(...arrays.map(a => a.length));
const out = Array(len);
for (let i = 0; i < len; i++) out[i] = fn(...arrays.map(a => a[i]), i);
return out;
}
// 4) Zip to object
export function zipObject(keys, values) {
const len = Math.min(keys.length, values.length);
const out = {};
for (let i = 0; i < len; i++) out[keys[i]] = values[i];
return out;
}
// 5) Unzip pairs [[a,b], ...] -> [a[], b[]]
export function unzip(pairs) {
const a = [], b = [];
for (const [x, y] of pairs) { a.push(x); b.push(y); }
return [a, b];
}
// 6) Longest zip with pad
export function zipLongest(pad, ...arrays) {
const len = Math.max(...arrays.map(a => a.length));
const out = Array(len);
for (let i = 0; i < len; i++) out[i] = arrays.map(a => (i < a.length ? a[i] : pad));
return out;
}
// 7) Transpose a 2D matrix
export function transpose(matrix) {
const rows = matrix.length;
const cols = rows ? Math.max(...matrix.map(r => r.length)) : 0;
const out = Array.from({ length: cols }, () => Array(rows));
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out[c][r] = matrix[r][c];
return out;
}
15) React, Node, and Data Workflow Examples
React: Controlled form values aligned with labels
const labels = ['Full Name', 'Email', 'Phone'];
const values = [fullName, email, phone];
<ul>
{zip(labels, values).map(([label, value]) => (
<li key={label}><strong>{label}:</strong> {value || '-'}</li>
))}
</ul>
Node: Merge CLI flags with defaults (by position)
const flags = ['--port', '--host'];
const provided = ['3000', '0.0.0.0'];
const args = zipObject(flags, provided);
// { '--port': '3000', '--host': '0.0.0.0' }
Data pipeline: Join columns and serialize
const headers = ['id','name','score'];
const rows = [
['1','Alice','99'],
['2','Bob','95']
];
const objects = rows.map(row => zipObject(headers, row));
// [{id:'1', name:'Alice', score:'99'}, ...]
Conclusion
Zipping is a tiny idea with huge leverage: pair arrays, build objects, transpose matrices, and stream data — cleanly and predictably. With a handful of small utilities (zip
, zipWith
, zipObject
, zipLongest
, unzip
, transpose
), you can drop Lodash for these tasks and keep your code portable and transparent.
The key choices are how to handle uneven lengths and whether you need eager arrays or lazy iterables. Start with the simple loop versions; reach for zipWith
when you want to compute as you combine; use generators (or async generators) for big or streaming data.
Pro Tip: Wrap these utilities in a tiny array-tools.ts
and import them across projects. Future-you (and your teammates) will thank you.
Call to Action (CTA)
What’s your favorite zip pattern — zipObject
, zipWith
, or a lazy generator?
Share a snippet or a tricky edge case in the comments. If this helped, bookmark it and send it to a teammate who keeps copy-pasting zip code from StackOverflow.
Leave a Reply