../

Arrays

Array in TypeScript 7.0 (also checked with 6.0) under strict, with the ES2025 built-ins of Node 24 LTS (every example also runs on Node 26 and Bun 1.4): creating, indexing, every method grouped by job, binary search (there is none built in), sorting, copying and aliasing, stacks, queues and deques, costs and V8's memory layout, the idioms DS&A solutions are built from, and typing. Text is in Strings, lookups by key in Maps & sets; Python's list is in Lists.

Creating

ExpressionResultNotes
[1, 2, 3][1, 2, 3]the usual way; infers number[]
[][]annotate it: const xs: number[] = []
Array.of(3), Array.of(1, 2)[3], [1, 2]no single-number trap
new Array(3), Array(3)3 empty slotslength 3 but no items: holes, not undefineds
new Array(3).fill(0)[0, 0, 0]fill(value, start?, end?) mutates and returns the array
Array.from({ length: 3 }, (_, i) => i * i)[0, 1, 4]a range with a map function: a fresh value per slot
Array.from("abc"), [..."abc"]["a", "b", "c"]any iterable; strings split by code point
[...new Set([1, 1, 2])][1, 2]dedupe, first occurrence wins
[...map.entries()], [...map.keys()][K, V][], K[]from a Map
Object.entries(obj)[string, V][]also Object.keys, Object.values
"a,b".split(",")["a", "b"]from text; see Strings
[...Array(n).keys()][0, 1, …, n - 1]indexes: the input of an argsort
Array.fromAsync(asyncIterable)Promise<T[]>awaits the items one by one
Array.isArray(x)booleannarrows x; false for typed arrays
const range = (n: number) =>
  Array.from({ length: n }, (_, i) => i);
range(4); // [0, 1, 2, 3]
 
const grid = Array.from({ length: 2 }, () =>
  new Array<number>(3).fill(0),
);
grid[0][0] = 1;
grid; // [[1, 0, 0], [0, 0, 0]]
 
const bad = new Array<number[]>(2).fill([0, 0, 0]);
bad[0][0] = 1;
bad; // [[1, 0, 0], [1, 0, 0]]: one row, twice
bad = new Array(2).fill([0, 0, 0]) then bad[0][0] = 1 [0] [1] outer 1 0 0 one row array grid = Array.from({ length: 2 }, () => new Array(3).fill(0)); grid[0][0] = 1 [0] [1] outer 1 0 0 0 0 0 row 0 row 1 bad prints [[1, 0, 0], [1, 0, 0]] grid prints [[1, 0, 0], [0, 0, 0]]
fill stores the same reference in every slot; Array.from calls the factory once per slot

new Array(n) makes holes, and most callbacks skip holes, so mapping over one does nothing:

const holes = new Array<number>(3);
holes.length; // 3
Object.keys(holes.map(() => 7)); // []: map skipped holes
Array.from({ length: 3 }, () => 7); // [7, 7, 7]

Typed arrays

Fixed-length arrays of one machine number type, zero-filled, with no holes. They take 1 to 8 bytes per item instead of a full JS value, and their sort() is numeric.

TypeHoldsOut-of-range writes
Int8Array, Int16Array, Int32Arraysigned integerswrap: Int32Array.of(2 ** 31)[0] is -2147483648
Uint8Array, Uint16Array, Uint32Arrayunsigned integerswrap: Uint8Array.of(256, -1) holds 0, 255
Uint8ClampedArray0 to 255clamp: 300 becomes 255, -5 becomes 0
Float32Array, Float64ArrayfloatsFloat64Array holds exactly what number holds
BigInt64Array, BigUint64Array64-bit integers as bigintwrap
const counts = new Int32Array(26); // all zeros
for (const ch of "banana") {
  counts[ch.charCodeAt(0) - 97]++;
}
counts[0]; // 3: the a's
counts[13]; // 2: the n's
 
new Int32Array([10, 9, 1, 2]).sort(); // [1, 2, 9, 10]
Int32Array.of(-1.9)[0]; // -1: truncates toward zero
Array.isArray(counts); // false
 
const xs = new Int32Array([1, 2, 3]);
xs.subarray(1)[0] = 9; // a view: shares memory
xs; // [1, 9, 3]
xs.slice(1)[0] = 7; // a copy
xs; // [1, 9, 3]

Indexing & slicing

There is no negative indexing with [] and no error past the end: both give undefined.

ExpressionOn ["p", "y", "t", "h", "o", "n"]Notes
xs[0]"p"typed string, though xs[99] is undefined at runtime
xs[-1]undefinedreads a property named "-1", not the last item
xs.at(-1), xs[xs.length - 1]"n"at is typed string | undefined
xs.slice(1, 4)["y", "t", "h"]end excluded; always a new array
xs.slice(-3), xs.slice(0, -1)["h", "o", "n"], ["p", "y", "t", "h", "o"]negatives count from the end
xs.slice(10)[]clamps, never throws
xs.slice(), [...xs]a shallow copy
const [a, b] = xs"p", "y"destructuring
const [, second] = xs"y"skip with a comma
const [first, ...rest] = xs"p", ["y", "t", "h", "o", "n"]the rest element must be last
const [x = "?"] = []"?"a default applies only to undefined
xs.length6writable: xs.length = 0 empties the array
xs.with(1, "Y")["p", "Y", "t", "h", "o", "n"]a copy; RangeError out of range
const xs = [10, 20, 30, 40, 50];
xs.at(-1); // 50
xs[-1]; // undefined
xs[7]; // undefined: no IndexError
xs.slice(1, 3); // [20, 30]
xs.slice(-2); // [40, 50]
 
const [first, ...rest] = xs;
first; // 10
rest; // [20, 30, 40, 50]
// no step slicing: filter by index
xs.filter((_, i) => i % 2 === 0); // [10, 30, 50]
xs.toReversed(); // [50, 40, 30, 20, 10]

Adding & removing

MethodReturnsCostCopying alternative
push(...items)the new lengthO(1) amortized[...xs, x]
pop()the item, or undefinedO(1)xs.slice(0, -1)
unshift(...items)the new lengthO(n)[x, ...xs]
shift()the item, or undefinedO(n) by the specxs.slice(1); for queues see Stacks, queues & deques
splice(i, k, ...items)the removed itemsO(n − i)toSpliced(i, k, ...items)
xs[i] = xO(1); past the end it grows the array and leaves holesxs.with(i, x)
concat(...arrays)a new arrayO(n + m)[...xs, ...ys]
fill(v, start?, end?)the same arrayO(k)Array.from({ length }, () => v)
copyWithin(target, start, end?)the same arrayO(k)none
xs.length = ntruncates, or pads with holesxs.slice(0, n)
const xs = ["a", "b", "c", "d"];
xs.splice(1, 2); // ["b", "c"]: the removed items
xs; // ["a", "d"]
xs.splice(1, 0, "x"); // []: insert at 1, nothing removed
xs; // ["a", "x", "d"]
xs.push("e", "f"); // 5: the new length
xs.pop(); // "f"
xs.toSpliced(0, 1); // ["x", "d", "e"]: xs is unchanged
 
const ys = [1, 2];
ys[4] = 5; // grows the array, leaving holes
ys.length; // 5

Searching & testing

MethodReturnsWhen nothing matchesNotes
indexOf(x, from?), lastIndexOfindex-1===: never finds NaN
includes(x, from?)booleanfalseSameValueZero: finds NaN
find(fn), findLast(fn)T | undefinedundefinedfirst match from the start or the end
findIndex(fn), findLastIndex(fn)index-1
some(fn)booleanfalsestops at the first true
every(fn)booleantrue on []stops at the first false
filter(fn)a new array[]keeps items where fn is truthy
a hand-written lowerBoundindexxs.lengthO(log n), sorted arrays only
const xs = [1, NaN, 3];
xs.indexOf(NaN); // -1: NaN !== NaN
xs.includes(NaN); // true
 
type User = { id: number; admin: boolean };
const users: User[] = [
  { id: 7, admin: true },
  { id: 9, admin: false },
];
const u = users.find((u) => u.id === 7); // User | undefined
u?.admin; // true
users.findIndex((u) => u.id === 8); // -1
users.some((u) => u.admin); // true
[1, 2].every((n) => n > 5); // false
([] as number[]).every((n) => n > 5); // true: vacuous

There is no built-in binary search on arrays: indexOf and includes scan. On a sorted array, write the two bounds once and reuse them. lowerBound is the first index whose item is >= x; upperBound the first whose item is > x. Both can return xs.length.

function lowerBound(xs: readonly number[], x: number) {
  let lo = 0;
  let hi = xs.length;
  while (lo < hi) {
    const mid = (lo + hi) >>> 1;
    if (xs[mid] < x) lo = mid + 1;
    else hi = mid;
  }
  return lo;
}
 
function upperBound(xs: readonly number[], x: number) {
  let lo = 0;
  let hi = xs.length;
  while (lo < hi) {
    const mid = (lo + hi) >>> 1;
    if (xs[mid] <= x) lo = mid + 1;
    else hi = mid;
  }
  return lo;
}
 
const xs = [1, 3, 3, 3, 7, 9];
lowerBound(xs, 3); // 1
upperBound(xs, 3); // 4
upperBound(xs, 3) - lowerBound(xs, 3); // 3: copies of 3
lowerBound(xs, 8); // 5: where 8 would go
lowerBound(xs, 10); // 6: past the end
lowerBound(xs, 3): first index i with xs[i] >= 3 [0] [1] [2] [3] [4] [5] end [6] 1 3 3 3 7 9 lo = 0 hi = 6 lo = 0, hi = 6: search all of [0, 6) 1 3 3 3 7 9 mid = 3 lo = 0 hi = 3 xs[3] = 3 is not < 3, so hi = mid = 3 1 3 3 3 7 9 mid = 1 lo = 0 hi = 1 xs[1] = 3 is not < 3, so hi = mid = 1 1 3 3 3 7 9 mid = 0 lo = hi = 1 xs[0] = 1 < 3, so lo = mid + 1 = 1 1 3 3 3 7 9 lo = hi = 1 lo === hi: return 1 upperBound: 4 3 halvings for 6 items: about log2(n)

1/5

lowerBound(xs, 3): lo = 0 and hi = 6. The answer is always in lo..hi, and hi = 6 would mean past the end.

(lo + hi) >>> 1 is exact while lo + hi stays below 2 ** 32, which holds for two indexes into any array shorter than 2 ** 31 items. >> 1 breaks at 2 ** 31: (2 ** 31) >> 1 is -1073741824. When you search a range of answers instead of an array, compute lo + Math.floor((hi - lo) / 2), exact up to 2 ** 53:

/** Smallest n in [lo, hi) with ok(n); hi if none. */
function firstTrue(
  lo: number,
  hi: number,
  ok: (n: number) => boolean,
) {
  while (lo < hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (ok(mid)) hi = mid;
    else lo = mid + 1;
  }
  return lo;
}
 
firstTrue(0, 100, (m) => m * m >= 50); // 8
firstTrue(0, 2 ** 50, (m) => m >= 3e12); // 3000000000000

ok must be monotonic: false up to some point, then true from there on.

Sorting & reordering

CallReturnsMutatesNotes
xs.sort(cmp?)the same arrayyeswithout cmp, compares items as strings
xs.toSorted(cmp?)a new arraynoES2023; works on readonly arrays
xs.reverse(), xs.toReversed()same / new arrayyes / no
xs.with(i, v)a new arrayno
typed.sort(cmp?)the same typed arrayyesnumeric by default

Rules to remember:

  • With no compare function, every item is converted to a string and the strings are compared by UTF-16 code units: [10, 9, 1, 2].sort() gives [1, 10, 2, 9]. undefined items go last (holes after them) and are never passed to a compare function.
  • cmp(a, b) returns a negative number to put a first, positive to put b first, 0 to keep their order. NaN counts as 0, so a single NaN item, or a - b on strings, silently leaves the array unsorted. a - b on bigint throws TypeError.
  • The sort is stable (required since ES2019): equal items keep their order, so you can sort by a minor key and then by a major one. V8 has used TimSort since V8 7.0 (Chrome 70), which is close to O(n) on input that is already mostly in order.
  • A comparator that returns a boolean, such as (a, b) => a > b, is wrong (it never says "a first"). TypeScript rejects it; JavaScript runs it, and Node 24 and Bun gave different orders.
const n = [10, 9, 1, 2];
n.toSorted(); // [1, 10, 2, 9]: as strings
n.toSorted((a, b) => a - b); // [1, 2, 9, 10]
n.toSorted((a, b) => b - a); // [10, 9, 2, 1]
[3, NaN, 1, 2].toSorted((a, b) => a - b); // [3, NaN, 1, 2]
 
// @ts-expect-error: a comparator must return a number
n.toSorted((a, b) => a > b);
 
const big = [3n, 1n, 2n];
big.toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0));
// [1n, 2n, 3n]
[10, 9, 1, 2].sort() [10, 9, 1, 2].sort((a, b) => a - b) numbers 10 9 1 2 as strings "10" "10" "9" "9" "1" "1" "2" "2" String(x) 49 57 49 50 49 57 49 50 first code unit "1" vs "10": first units tie (49 = 49), "1" runs out first, so "1" < "10" "10" vs "2": 49 < 50 decides; the 0 never meets anything result 1 10 2 9 [1, 10, 2, 9]: numbers in text order 1 2 9 10 [1, 2, 9, 10]: a - b < 0 puts a first 10 - 9 = 1 > 0, so 9 goes before 10 no strings are made: the numbers are compared

1/5

[10, 9, 1, 2].sort() with no compare function converts every item to a string first: "10", "9", "1" and "2".

Sort objects by several keys with ||, which moves to the next key when one gives 0:

type P = { name: string; age: number };
const people: P[] = [
  { name: "cy", age: 31 },
  { name: "ann", age: 31 },
  { name: "bo", age: 25 },
];
const byAgeDesc = (a: P, b: P) =>
  b.age - a.age || a.name.localeCompare(b.name);
people.toSorted(byAgeDesc).map((p) => p.name);
// ["ann", "cy", "bo"]
 
// stable: equal keys keep their input order
const rows = [["b", 2], ["a", 1], ["c", 2]] as const;
rows.toSorted((x, y) => x[1] - y[1]).map((r) => r[0]);
// ["a", "b", "c"]
 
// argsort: indexes in value order, best first
const scores = [70, 95, 80];
[...scores.keys()].sort((i, j) => scores[j] - scores[i]);
// [1, 2, 0]

Text sorts by code unit unless you pass a collator:

["é", "z", "a"].toSorted(); // ["a", "z", "é"]
["é", "z", "a"].toSorted((a, b) => a.localeCompare(b));
// ["a", "é", "z"]
 
const coll = new Intl.Collator("en", { numeric: true });
["v10", "v9", "v1"].toSorted(coll.compare);
// ["v1", "v9", "v10"]

Iterating & transforming

MethodReturnsNotes
for (const x of xs)statementbreak, continue, await; visits holes as undefined
for (const [i, x] of xs.entries())statementindex and item
forEach(fn)undefinedno break; ignores returned promises; skips holes
map(fn)U[], same lengthfn(item, index, array); skips holes
filter(fn)T[]narrows with a type predicate
flatMap(fn)U[]map, then flatten one level; return [] to drop an item
flat(depth = 1)a new arrayflat(Infinity) flattens fully
keys(), values(), entries()iteratorskeys() includes holes
join(sep = ",")stringnull and undefined become ""
const words = ["a b", "c"];
words.map((w) => w.length); // [3, 1]
words.flatMap((w) => w.split(" ")); // ["a", "b", "c"]
[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]]
 
// flatMap as filter + map in one pass
const nums = ["1", "x", "3"].flatMap((s) => {
  const n = Number(s);
  return Number.isNaN(n) ? [] : [n];
});
nums; // [1, 3]

Removing items while looping forward by index skips the item after each removal:

const xs = [1, 2, 2, 3];
for (let i = 0; i < xs.length; i++) {
  if (xs[i] === 2) xs.splice(i, 1); // ❌ skips the next item
}
xs; // [1, 2, 3]
 
const ys = [1, 2, 2, 3];
for (let i = ys.length - 1; i >= 0; i--) {
  if (ys[i] === 2) ys.splice(i, 1); // ✅ walk backward
}
ys; // [1, 3]
[1, 2, 2, 3].filter((x) => x !== 2); // ✅ [1, 3], a copy

Reducing & grouping

MethodReturnsNotes
reduce(fn, init)the accumulatorTypeError on [] without init; type it with reduce<T>
reduceRight(fn, init)the accumulatorwalks from the end
Object.groupBy(xs, fn)Partial<Record<K, T[]>>ES2024; a null-prototype object
Map.groupBy(xs, fn)Map<K, T[]>ES2024; keys can be any value
Math.max(...xs), Math.min(...xs)number-Infinity / Infinity on []; RangeError past about 125,000 items on Node
const prices = [3, 4.5, 10];
prices.reduce((sum, p) => sum + p, 0); // 17.5
([] as number[]).reduce((a, b) => a + b, 0); // 0
 
type Counts = Record<string, number>;
const counts = ["a", "b", "a"].reduce<Counts>(
  (acc, k) => {
    acc[k] = (acc[k] ?? 0) + 1;
    return acc;
  },
  {},
);
counts; // { a: 2, b: 1 }
 
Math.max(...prices); // 10
prices.reduce((m, p) => Math.max(m, p), -Infinity); // 10
const stock = [
  { name: "apple", qty: 0 },
  { name: "pear", qty: 4 },
];
const g = Object.groupBy(stock, (i) =>
  i.qty > 0 ? "inStock" : "soldOut",
);
g.inStock?.map((i) => i.name); // ["pear"]
 
const m = Map.groupBy(stock, (i) => i.qty > 0);
m.get(false)?.map((i) => i.name); // ["apple"]

Copying & aliasing

Assignment never copies, and a function that receives an array receives the caller's array.

ExpressionGivesDepth
const ys = xsthe same array under a second namenone
[...xs], xs.slice(), Array.from(xs)a new array holding the same itemsshallow
structuredClone(xs)items copied recursively, Maps and Dates includeddeep; functions throw DataCloneError, class instances come back as plain objects
xs.length = 0 vs xs = []empties it for everyone vs rebinds only xs
xs.push(...ys) vs xs = [...xs, ...ys]extends in place vs builds a new array
const a = [[1, 2], [3]];
const b = a; // an alias
const c = [...a]; // new outer array, same inner arrays
const d = structuredClone(a); // nothing shared
a[0].push(99);
a.push([4]);
b; // [[1, 2, 99], [3], [4]]
c; // [[1, 2, 99], [3]]
d; // [[1, 2], [3]]
 
function tag(items: string[]) {
  items.push("seen"); // mutates the caller's array
}
const names = ["ann"];
tag(names);
names; // ["ann", "seen"]

Every mutating method has a copying counterpart, and on a readonly T[] TypeScript removes the mutating ones, so the compiler enforces this table:

MutatesCopying counterpart
push(x), unshift(x)[...xs, x], [x, ...xs], concat
pop(), shift()slice(0, -1), slice(1)
splice(i, k, ...items)toSpliced(i, k, ...items)
sort(cmp)toSorted(cmp)
reverse()toReversed()
xs[i] = vwith(i, v)
fill(v)Array.from({ length }, () => v)
copyWithin, length = nnone, slice(0, n)
map, filter, flatMap, flat, slice, concatalready copy (shallowly)
const frozen: readonly number[] = [3, 1, 2];
frozen.toSorted(); // [1, 2, 3]: a new number[]
// @ts-expect-error: sort does not exist on readonly number[]
frozen.sort();
frozen; // [1, 2, 3]: readonly is erased, so it still ran
 
const locked = Object.freeze([3, 1, 2]); // readonly number[]
let err = "";
try {
  // @ts-expect-error: and at runtime, sort throws
  locked.sort();
} catch (e) {
  err = (e as Error).name;
}
err; // "TypeError"

readonly exists only for the compiler: if a write slips past it (an any, a cast, a silenced error), the array changes anyway. Object.freeze enforces it at runtime, and TypeScript types the result as readonly T[].

Stacks, queues & deques

A stack is a plain array: push and pop work at the end, in O(1).

const st: number[] = [];
st.push(1);
st.push(2);
st.at(-1); // 2: peek
st.pop(); // 2
st.pop(); // 1
st.pop(); // undefined: no error on empty
st.length === 0; // true

A queue is the problem: shift() removes the front, and the spec defines it as moving every remaining item one slot left. Keep a head index instead, and only read forward:

const q = [10, 20, 30, 40, 50];
let head = 0;
q[head++]; // 10: dequeue in O(1)
q[head++]; // 20
q.push(60); // enqueue
q.length - head; // 4: still queued
q.slice(head); // [30, 40, 50, 60]
dequeue twice, then push(60) q.shift() q[head++] [0] [0] [1] [1] [2] [2] [3] [3] [4] [4] [5] [5] 10 20 30 40 50 moved: 0 10 20 30 40 50 head = 0 moved: 0 20 30 40 50 returns 10 moved: 4 10 20 30 40 50 head = 1 returns 10 moved: 0 dead 30 40 50 returns 20 moved: 7 10 20 30 40 50 head = 2 returns 20 moved: 0 dead 30 40 50 60 moved: 7 10 20 30 40 50 60 head = 2 moved: 0 dead 30 40 50 60 moved: 7 10 20 30 40 50 60 head = 2 moved: 0 draining n items with shift moves (n - 1) + (n - 2) + … + 1 items: O(n²) with a head index nothing moves: O(n) for the whole drain push appends at the end in both: O(1) amortized each shift moves every remaining item one slot left a queue stored in an array: the front is index 0

1/5

Both queues start as [10, 20, 30, 40, 50] with the front at index 0. The bottom one also keeps head = 0.

How much shift really costs depends on the engine. Measured on Node 24, whose V8 engine Chrome also runs: about 0.1 µs per shift up to roughly 16,000 items, because V8 then moves the array's start instead of copying the items (an engine detail; the cutoff matches a backing store of 128 KB). From 16,384 items, every shift copies the rest: about 2 µs at that size and 20 µs at 128,000. A BFS that shifted a 100,000-item queue took 0.37 s, against 4 ms with a head index. Bun (JavaScriptCore) kept shift cheap at every size tried. unshift and a mid-array splice were linear in both.

For both ends, use a ring buffer: a power-of-two array, a head index and a size, with indexes wrapped by a bit mask.

class Deque<T> {
  private buf: (T | undefined)[] = new Array(8);
  private head = 0;
  private n = 0;
 
  get size() {
    return this.n;
  }
  pushBack(x: T) {
    this.grow();
    this.buf[this.at(this.n++)] = x;
  }
  pushFront(x: T) {
    this.grow();
    this.head = this.at(-1);
    this.buf[this.head] = x;
    this.n++;
  }
  popFront(): T | undefined {
    if (this.n === 0) return undefined;
    const x = this.buf[this.head];
    this.buf[this.head] = undefined; // let GC take it
    this.head = this.at(1);
    this.n--;
    return x;
  }
  popBack(): T | undefined {
    if (this.n === 0) return undefined;
    const i = this.at(--this.n);
    const x = this.buf[i];
    this.buf[i] = undefined;
    return x;
  }
  peekFront(): T | undefined {
    return this.n ? this.buf[this.head] : undefined;
  }
  peekBack(): T | undefined {
    if (this.n === 0) return undefined;
    return this.buf[this.at(this.n - 1)];
  }
  private at(k: number) {
    return (this.head + k) & (this.buf.length - 1);
  }
  private grow() {
    if (this.n < this.buf.length) return;
    const old = this.buf;
    const mask = old.length - 1;
    this.buf = new Array(old.length * 2);
    for (let k = 0; k < this.n; k++) {
      this.buf[k] = old[(this.head + k) & mask];
    }
    this.head = 0;
  }
}
 
const d = new Deque<number>();
d.pushBack(1);
d.pushBack(2);
d.pushFront(0);
d.popBack(); // 2
d.popFront(); // 0
d.peekFront(); // 1
d.size; // 1

(head - 1) & mask wraps -1 to the last slot because & works on 32-bit two's complement: -1 & 7 is 7.

Costs & memory

OperationCostNotes
xs[i], xs[i] = x, xs.lengthO(1)
push, popO(1) amortizedthe end is cheap
shift, unshiftO(n)V8 shifts small arrays (under about 16,000 items) in O(1)
splice(i, k, ...)O(n − i + k)items after i move
indexOf, includes, find, someO(n)a Set answers has in O(1) on average
slice, spread, concat, toSpliced, withO(n)copies
sort, toSortedO(n log n)about O(n) on mostly sorted input
reverse, fill, map, filter, reduce, joinO(n)
xs = [...xs, x] in a loopO(n²) totalpush instead
a hand-written lowerBoundO(log n)the array must be sorted

V8 (Node, Deno, Chrome) stores an array's items in a separate backing store and tags the array with an elements kind, which picks the fastest representation the items allow. The rest of this section is engine detail, not spec, measured on Node 24 and 26 with node --allow-natives-syntax and %DebugPrint(xs).

small integers doubles any values specific, fastest general PACKED_SMI_ELEMENTS [1, 2, 3] PACKED_DOUBLE_ELEMENTS [1, 2, 4.5] PACKED_ELEMENTS [1, 2, "x"] HOLEY_SMI_ELEMENTS new Array(3) HOLEY_DOUBLE_ELEMENTS [1, , 4.5] HOLEY_ELEMENTS [1, , "x"] 4.5 "x" 4.5 "x" a hole a hole a hole fill() Smi: small integer · DOUBLE: unboxed floats · ELEMENTS: pointers to any values dashed: a fill() over the whole array makes it packed again (Node 24 and 26, not Node 22)
Kinds move right (more general values) or down (holes), never back, except that a fill() over the whole array repacks it on Node 24+. V8 tracks 21 kinds; these are the common six
const xs = [1, 2, 3]; // PACKED_SMI_ELEMENTS
xs.push(4.5); // PACKED_DOUBLE_ELEMENTS
xs[3] = 4; // still PACKED_DOUBLE: kinds never go back
 
const ws: (number | string)[] = [1, 2];
ws.push("x"); // PACKED_ELEMENTS: stores pointers
 
const ys = [1, 2, 3];
ys[5] = 6; // HOLEY_SMI_ELEMENTS
ys[3] = 4;
ys[4] = 5; // still HOLEY, though no hole is left
  • Small integers (Smis) are the cheapest. NaN, -0, fractions and integers outside the 32-bit range (on Node) make an array DOUBLE; any other value makes it ELEMENTS, which stores pointers.
  • A hole (new Array(n), writing past the end, delete xs[i], xs.length = bigger) makes the array HOLEY for good, and reading a hole has to check the prototype chain. The one exception, noted by V8 in February 2025: fill over the whole array makes it packed again, so new Array(n).fill(0) is PACKED_SMI_ELEMENTS on Node 24 and 26 but HOLEY_SMI_ELEMENTS on Node 22.
  • Array.from({ length: n }, fn) and a loop of push from [] build packed arrays.

When push finds the backing store full, V8 allocates a new one of n + (n >> 1) + 16 slots for the needed length n and copies the old items over:

const xs: number[] = [] xs.push(1) xs.push(2) … xs.push(17) xs.push(18) keep pushing length 0 · capacity 0 length 1 · capacity 17 length 17 · capacity 17 length 18 · capacity 43 capacity = n + (n >> 1) + 16 an empty backing store: no slots yet 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 17 slots: 1 + (1 >> 1) + 16 items 2 to 17 fill spare slots: no allocation, O(1) each 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 old block: 17 slots, full copy 17 items new block: 18 + (18 >> 1) + 16 = 43 slots; item 18 goes in slot 17 at length 1 17 slots at length 18 43 slots at length 44 82 slots at length 83 140 slots the growth factor tends to 1.5, so all the copying adds up to O(n) engine detail: measured with %DebugPrint on Node 22, 24 and 26

1/5

const xs: number[] = [] has length 0 and an empty backing store.

NeedUse insteadWhy
fast membership, dedupeSetO(1) average has; see Maps & sets
lookup by keyMapO(1) average by key instead of a scan
a queuea head index, or the Deque aboveshift is O(n)
the smallest item, repeatedlya binary heapO(log n) push and pop; see Heaps
millions of numbersInt32Array, Float64Array4 or 8 bytes per item, numeric sort, no holes
a read-only view for callersreadonly T[]the compiler removes the mutating methods
immutable updates (React state)toSorted, toSpliced, with, spreadnew arrays, so === sees the change

Iterator helpers

Iterators gained lazy, chainable methods in ES2025 (Node 22+), so a generator, a Map or a Set can be transformed without building intermediate arrays. TypeScript types them in lib: es2025.

MethodReturnsNotes
Iterator.from(x)an iterator with helperswraps any iterable or iterator
.map(fn), .filter(fn), .flatMap(fn)lazy iteratorsfn(value, index)
.take(n), .drop(n)lazy iteratorsmake infinite iterators usable
.toArray()T[]consumes the iterator
.reduce, .some, .every, .find, .forEacha valueconsume, stopping early where they can
function* naturals() {
  let n = 0;
  while (true) yield n++;
}
 
naturals()
  .filter((n) => n % 2 === 0)
  .map((n) => n * 10)
  .take(3)
  .toArray(); // [0, 20, 40]
 
const ages = new Map([["ann", 31], ["bo", 17]]);
ages
  .entries()
  .filter(([, age]) => age >= 18)
  .map(([name]) => name)
  .toArray(); // ["ann"]
 
Iterator.from(new Set([1, 50, 200])).find((n) => n > 10);
// 50

DS&A idioms

The array moves that problem solutions are built from, and the trap in each. The patterns themselves, with TypeScript, JavaScript and Python side by side, are in the DS&A sheets.

JobTypeScriptWatch outMore
Stackst.push(x), st.pop(), peek st.at(-1), empty st.length === 0pop() on empty returns undefined; if (st.length) tests non-emptyStacks
Queue, BFSq.push(x), const x = q[head++], loop while (head < q.length)q.shift() is O(n) on big queues: BFS turns O(n²)Queues & deques, Graphs
Deque, sliding window maxthe ring-buffer Deque, or an index array with a head pointerstore indexes, not values, to know when one leaves the windowMonotonic stack & deque
Priority queuea binary heap class: there is no built-inpush then sort on every insert is O(n log n) per insertHeaps, TS heaps
Top kxs.toSorted(cmp).slice(0, k)O(n log n); a size-k heap is O(n log k)Top-k
Sorted lookuplowerBound(xs, x), then i < xs.length && xs[i] === xindexOf on a sorted array is still O(n)Lower & upper bound
Search the answerfirstTrue(lo, hi, ok)ok must be monotonic; (lo + hi) >> 1 breaks past 2 ** 31On the answer
Prefix sumspre[i + 1] = pre[i] + xs[i] in a pre of length n + 1sums past 2 ** 53 lose precision: use bigintPrefix sums
Difference arraydiff[l] += v; diff[r + 1] -= v, then a running sumsize it n + 1 so r + 1 existsPrefix sums
Two pointerslet i = 0, j = xs.length - 1; while (i < j)swap with [xs[i], xs[j]] = [xs[j], xs[i]]Two pointers
DP tableArray.from({ length: n + 1 }, () => new Array(m + 1).fill(0))new Array(n).fill(new Array(m).fill(0)) shares one rowBottom-up DP
Countingnew Int32Array(26), cnt[s.charCodeAt(i) - 97]++arrays are Map keys by identity: key by cnt.join(",")Group anagrams
SentinelsInfinity, -InfinityMath.max(...[]) is -Infinity; the spread throws on huge arraysNumbers
Grid neighborsfor (const [dr, dc] of DIRS) with DIRS as const, then bounds checksgrid[-1] is undefined, so grid[-1][c] throws TypeErrorGrids as graphs
Intervalsivs.sort((a, b) => a[0] - b[0])the default sort compares "10,2" with "9,1" as textIntervals
Argsort[...xs.keys()].sort((i, j) => xs[i] - xs[j])stable, so ties keep index orderArgsort
Backtrackingpath.push(x), recurse, path.pop(); record out.push([...path])out.push(path) stores one array that ends up emptyBacktracking
Memo keysmemo.get(`${i},${j}`) or i * cols + j[i, j] as a Map key never matches a new [i, j]Maps & sets
Deep recursionan explicit stack: const st = [root]; while (st.length)Node overflows near 12,500 frames of a tiny function, fewer with more localsDepth limits
const xs = [2, 4, 1, 5];
const pre = new Array<number>(xs.length + 1).fill(0);
for (let i = 0; i < xs.length; i++) {
  pre[i + 1] = pre[i] + xs[i];
}
pre; // [0, 2, 6, 7, 12]
pre[3] - pre[1]; // 5: xs[1] + xs[2] in O(1)
 
const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]] as const;
const grid = ["..#", "...", "#.."];
const [r, c] = [0, 0];
const nbrs: [number, number][] = [];
for (const [dr, dc] of DIRS) {
  const nr = r + dr;
  const nc = c + dc;
  if (nr < 0 || nr >= grid.length) continue;
  if (nc < 0 || nc >= grid[0].length) continue;
  if (grid[nr][nc] === ".") nbrs.push([nr, nc]);
}
nbrs; // [[1, 0], [0, 1]]
 
function subsets(nums: readonly number[]): number[][] {
  const out: number[][] = [];
  const path: number[] = [];
  const go = (i: number): void => {
    if (i === nums.length) {
      out.push([...path]); // copy: path keeps changing
      return;
    }
    go(i + 1); // skip nums[i]
    path.push(nums[i]); // take nums[i]
    go(i + 1);
    path.pop(); // undo
  };
  go(0);
  return out;
}
subsets([1, 2]); // [[], [2], [1], [1, 2]]

Typing

TypeMeaning
T[], Array<T>the same type; write (A | B)[] or Array<A | B> for unions
readonly T[], ReadonlyArray<T>no mutating methods, no index assignment; accepts T[]
[string, number]a tuple: fixed length, a type per position
[x: number, y: number]labeled elements, for hover hints and docs
[string, number?]an optional last element: length is 1 | 2
[string, ...number[]]a rest element: a string, then any numbers
readonly [number, number]a read-only tuple, what as const infers
(typeof xs)[number]the element type of an array value
Int32Array, Float64Arraytyped arrays: their own types, not number[]
type Point = readonly [x: number, y: number];
const p: Point = [1, 2];
const [px, py] = p;
px + py; // 3
 
type Row = [id: string, ...scores: number[]];
const r: Row = ["ann", 9, 7];
r.length; // 3
 
const dirs = ["n", "e", "s", "w"] as const;
type Dir = (typeof dirs)[number]; // "n" | "e" | "s" | "w"
const turn = (d: Dir): Dir =>
  dirs[(dirs.indexOf(d) + 1) % 4];
turn("w"); // "n"
 
function sum(xs: readonly number[]) {
  return xs.reduce((a, b) => a + b, 0);
}
sum([1, 2, 3]); // 6: number[] fits readonly number[]

Parameters you only read should be readonly T[]: callers can pass either kind, and the body can't mutate their array by accident. The reverse fails: a readonly number[] can't go where number[] is expected.

noUncheckedIndexedAccess

Under plain strict, xs[i] on a number[] is typed number, even though it is undefined past the end. With "noUncheckedIndexedAccess": true it is typed number | undefined, which matches runtime and forces a check or a ! on every index read. Index-heavy DS&A code then needs one of these:

WriteWhen
for (const x of xs), xs.entries()no index needed: never undefined
xs[i]!you know i is in range (a loop bound)
xs[i] ?? 0out of range has a sensible default
const x = xs[i]; if (x === undefined) …the narrowing is the logic
tuples like [number, number]known positions stay precise
const xs = [5, 7, 9];
let total = 0;
for (let i = 0; i < xs.length; i++) {
  total += xs[i]!; // `!` keeps this compiling with the flag
}
total; // 21
const last = xs[xs.length - 1] ?? 0;
last; // 9
xs.at(5) ?? -1; // -1

! is unchecked: it silences the type without testing anything, so keep it to reads guarded by a loop bound. The other examples on this page are written for plain strict.

Inferred type predicates (TS 5.5+)

TypeScript infers x is T from simple arrow functions, so filter narrows without a hand-written predicate. filter(Boolean) still doesn't narrow.

const maybe = [1, undefined, 3];
const nums = maybe.filter((n) => n !== undefined);
nums; // [1, 3], typed number[]
 
const mixed = ["a", null, "b"];
const strs = mixed.filter((s) => s != null);
strs; // ["a", "b"], typed string[]

Empty arrays

const xs = [] starts as an evolving any[] that takes its type from the pushes that follow, until it escapes (returned or passed on). In object literals and class fields an empty [] is never[]. Annotate empty arrays: const xs: number[] = [], or new Array<number>().

Coming from Python

Python listTypeScript Array
len(xs)xs.length
xs.append(x), xs.extend(ys)xs.push(x), xs.push(...ys); the spread throws on very long ys
xs.pop()xs.pop(): undefined on empty, no IndexError
xs.pop(0), xs.insert(0, x)xs.shift(), xs.unshift(x); queues use a head index
xs.insert(i, x), del xs[i:j]xs.splice(i, 0, x), xs.splice(i, j - i)
xs[i:j] = ysxs.splice(i, j - i, ...ys)
xs[-1]xs.at(-1): xs[-1] is undefined
xs[a:b], xs[::-1], xs[::2]xs.slice(a, b), xs.toReversed(), xs.filter((_, i) => i % 2 === 0)
xs[10] raises IndexErrorxs[10] is undefined
x in xs, xs.index(x)xs.includes(x), xs.indexOf(x) (-1, not ValueError)
xs.count(x)xs.filter((y) => y === x).length
xs.sort(), sorted(xs)xs.sort((a, b) => a - b), xs.toSorted((a, b) => a - b): the default sort is text order
sorted(xs, key=f)xs.toSorted((a, b) => f(a) - f(b)) for numeric keys
key=lambda p: (p.a, p.b)(p, q) => p.a - q.a || p.b - q.b
bisect_left(xs, x)a hand-written lowerBound
[f(x) for x in xs if p(x)]xs.filter(p).map(f) or one flatMap
enumerate(xs)xs.entries()
zip(a, b)a.map((x, i) => [x, b[i]]): no built-in
sum(xs), max(xs)xs.reduce((s, x) => s + x, 0), Math.max(...xs)
any(...), all(...)xs.some(p), xs.every(p)
[0] * nnew Array(n).fill(0)
[[0] * m for _ in range(n)]Array.from({ length: n }, () => new Array(m).fill(0))
list(range(n))[...Array(n).keys()]
xs.copy(), copy.deepcopy(xs)[...xs], structuredClone(xs)
xs.clear()xs.length = 0
collections.dequea head index, or a ring-buffer Deque
heapqno built-in: a Heap class; see Heaps
tuplereadonly [a: A, b: B]: a type only, still an array at runtime; see Python tuples
list[int], Sequence[int]number[], readonly number[]
array.array("i")Int32Array

Recipes

Two sum on a sorted array

When the input is sorted and you need a pair with a given sum, in O(n) and O(1) space.

function twoSumSorted(
  xs: readonly number[],
  target: number,
): [number, number] | undefined {
  let i = 0;
  let j = xs.length - 1;
  while (i < j) {
    const s = xs[i] + xs[j];
    if (s === target) return [i, j];
    if (s < target) i++;
    else j--;
  }
  return undefined;
}
 
twoSumSorted([1, 3, 4, 6, 9], 10); // [0, 4]
twoSumSorted([1, 2], 7); // undefined

Merge intervals

When overlapping ranges must collapse: calendars, coverage, "insert interval".

type Interval = [start: number, end: number];
 
function merge(ivs: readonly Interval[]): Interval[] {
  const sorted = ivs.toSorted((a, b) => a[0] - b[0]);
  const out: Interval[] = [];
  for (const [s, e] of sorted) {
    const last = out.at(-1);
    if (last && s <= last[1]) last[1] = Math.max(last[1], e);
    else out.push([s, e]);
  }
  return out;
}
 
merge([[8, 10], [1, 3], [2, 6], [15, 18]]);
// [[1, 6], [8, 10], [15, 18]]

Product of array except self

When each answer combines everything to its left and everything to its right, without division.

function productExceptSelf(xs: readonly number[]) {
  const n = xs.length;
  const out = new Array<number>(n).fill(1);
  let left = 1;
  for (let i = 0; i < n; i++) {
    out[i] = left;
    left *= xs[i];
  }
  let right = 1;
  for (let i = n - 1; i >= 0; i--) {
    out[i] *= right;
    right *= xs[i];
  }
  return out;
}
 
productExceptSelf([1, 2, 3, 4]); // [24, 12, 8, 6]

Rotate in place

When an array must rotate right by k with O(1) extra space: three reversals.

function reverse(xs: number[], i: number, j: number) {
  for (; i < j; i++, j--) [xs[i], xs[j]] = [xs[j], xs[i]];
}
 
function rotate(xs: number[], k: number) {
  const n = xs.length;
  if (n === 0) return;
  k = ((k % n) + n) % n; // also handles k < 0
  reverse(xs, 0, n - 1);
  reverse(xs, 0, k - 1);
  reverse(xs, k, n - 1);
}
 
const xs = [1, 2, 3, 4, 5, 6, 7];
rotate(xs, 3);
xs; // [5, 6, 7, 1, 2, 3, 4]

[...xs.slice(-k), ...xs.slice(0, -k)] is the copying version; with k = 0 it needs a guard, because slice(-0) is slice(0), the whole array.

Move zeros

When items must be compacted in place while keeping their order (remove element, dedupe a sorted array).

function moveZeros(xs: number[]) {
  let w = 0; // next write position
  for (const x of xs) if (x !== 0) xs[w++] = x;
  xs.fill(0, w);
}
 
const xs = [0, 1, 0, 3, 12];
moveZeros(xs);
xs; // [1, 3, 12, 0, 0]

Maximum subarray

When you need the best sum of a contiguous run (Kadane's algorithm).

function maxSubarray(xs: readonly number[]) {
  let best = -Infinity;
  let cur = 0;
  for (const x of xs) {
    cur = Math.max(x, cur + x); // extend, or start over
    best = Math.max(best, cur);
  }
  return best;
}
 
maxSubarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]); // 6
maxSubarray([-3, -1]); // -1

Daily temperatures

When each item needs the next greater item to its right: a monotonic stack of indexes.

function nextWarmer(temps: readonly number[]) {
  const out = new Array<number>(temps.length).fill(0);
  const st: number[] = []; // indexes, temps decreasing
  temps.forEach((t, i) => {
    while (st.length && temps[st.at(-1)!] < t) {
      const j = st.pop()!;
      out[j] = i - j;
    }
    st.push(i);
  });
  return out;
}
 
nextWarmer([73, 74, 75, 71, 69, 72, 76, 73]);
// [1, 1, 4, 2, 1, 1, 0, 0]

Sliding window maximum

When you need the max of every window of k items in O(n): a monotonic deque of indexes, kept in a plain array with a head pointer.

function windowMax(xs: readonly number[], k: number) {
  const dq: number[] = []; // indexes, values decreasing
  let head = 0;
  const out: number[] = [];
  xs.forEach((x, i) => {
    if (dq[head] === i - k) head++; // left the window
    while (dq.length > head && xs[dq.at(-1)!] <= x) {
      dq.pop();
    }
    dq.push(i);
    if (i >= k - 1) out.push(xs[dq[head]]);
  });
  return out;
}
 
windowMax([1, 3, -1, -3, 5, 3, 6, 7], 3);
// [3, 3, 5, 5, 6, 7]

Spiral order and rotating a grid

When a matrix is walked layer by layer, or turned 90 degrees.

function spiral(m: readonly (readonly number[])[]) {
  const out: number[] = [];
  let top = 0;
  let bottom = m.length - 1;
  let left = 0;
  let right = (m[0]?.length ?? 0) - 1;
  while (top <= bottom && left <= right) {
    for (let c = left; c <= right; c++) out.push(m[top][c]);
    for (let r = top + 1; r <= bottom; r++) {
      out.push(m[r][right]);
    }
    if (top < bottom && left < right) {
      for (let c = right - 1; c > left; c--) {
        out.push(m[bottom][c]);
      }
      for (let r = bottom; r > top; r--) {
        out.push(m[r][left]);
      }
    }
    top++;
    bottom--;
    left++;
    right--;
  }
  return out;
}
 
spiral([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
// [1, 2, 3, 6, 9, 8, 7, 4, 5]
 
const m = [[1, 2, 3], [4, 5, 6]];
// transpose, then reverse each row: a clockwise turn
m[0].map((_, c) => m.map((row) => row[c]).toReversed());
// [[4, 1], [5, 2], [6, 3]]

Chunk and zip

When an API takes at most n items at a time, or two parallel arrays must become pairs.

function chunk<T>(xs: readonly T[], size: number): T[][] {
  if (!Number.isInteger(size) || size < 1) {
    throw new RangeError(`size must be >= 1, got ${size}`);
  }
  return Array.from(
    { length: Math.ceil(xs.length / size) },
    (_, i) => xs.slice(i * size, i * size + size),
  );
}
 
function zip<A, B>(as: readonly A[], bs: readonly B[]) {
  const n = Math.min(as.length, bs.length);
  return Array.from(
    { length: n },
    (_, i): [A, B] => [as[i], bs[i]],
  );
}
 
chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
zip(["a", "b", "c"], [1, 2]); // [["a", 1], ["b", 2]]
Object.fromEntries(zip(["x", "y"], [10, 20]));
// { x: 10, y: 20 }

Partition with a type predicate

When one pass should split items into matches and the rest; with a predicate both halves are narrowed.

function partition<T, S extends T>(
  xs: readonly T[],
  pred: (x: T) => x is S,
): [S[], Exclude<T, S>[]];
function partition<T>(
  xs: readonly T[],
  pred: (x: T) => boolean,
): [T[], T[]];
function partition<T>(
  xs: readonly T[],
  pred: (x: T) => boolean,
): [T[], T[]] {
  const yes: T[] = [];
  const no: T[] = [];
  for (const x of xs) (pred(x) ? yes : no).push(x);
  return [yes, no];
}
 
const isEven = (n: number) => n % 2 === 0;
const [evens, odds] = partition([1, 2, 3, 4], isEven);
evens; // [2, 4]
odds; // [1, 3]
const mixed: (string | number)[] = ["a", 1, "b"];
const [strs, nums] = partition(
  mixed,
  (x) => typeof x === "string",
); // string[] and number[]
strs; // ["a", "b"]

Sort by several keys, mixed directions

When the order has tie-breakers and some keys run descending; by() builds one comparator.

type Key<T> = (x: T) => string | number | Date;
type Dir = "asc" | "desc";
 
function by<T>(
  ...keys: [Key<T>, Dir?][]
): (a: T, b: T) => number {
  return (a, b) => {
    for (const [key, dir = "asc"] of keys) {
      const x = key(a).valueOf();
      const y = key(b).valueOf();
      if (x === y) continue;
      const cmp = x < y ? -1 : 1;
      return dir === "asc" ? cmp : -cmp;
    }
    return 0;
  };
}
 
type Task = { done: boolean; due: number; title: string };
const tasks: Task[] = [
  { done: true, due: 1, title: "ship" },
  { done: false, due: 5, title: "test" },
  { done: false, due: 2, title: "fix" },
];
const order = by<Task>(
  [(t) => Number(t.done)], // open first
  [(t) => t.due], // then soonest
);
tasks.toSorted(order).map((t) => t.title);
// ["fix", "test", "ship"]

Move an item

When reordering without mutating, for example after a drag and drop.

function move<T>(
  xs: readonly T[],
  from: number,
  to: number,
): T[] {
  if (from < 0 || from >= xs.length) return [...xs];
  const item = xs[from];
  return xs.toSpliced(from, 1).toSpliced(to, 0, item);
}
 
move(["a", "b", "c", "d"], 0, 2); // ["b", "c", "a", "d"]
move(["a", "b", "c", "d"], 3, 0); // ["d", "a", "b", "c"]

References