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
| Expression | Result | Notes |
|---|---|---|
[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 slots | length 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) | boolean | narrows 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, twicenew 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.
| Type | Holds | Out-of-range writes |
|---|---|---|
Int8Array, Int16Array, Int32Array | signed integers | wrap: Int32Array.of(2 ** 31)[0] is -2147483648 |
Uint8Array, Uint16Array, Uint32Array | unsigned integers | wrap: Uint8Array.of(256, -1) holds 0, 255 |
Uint8ClampedArray | 0 to 255 | clamp: 300 becomes 255, -5 becomes 0 |
Float32Array, Float64Array | floats | Float64Array holds exactly what number holds |
BigInt64Array, BigUint64Array | 64-bit integers as bigint | wrap |
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.
| Expression | On ["p", "y", "t", "h", "o", "n"] | Notes |
|---|---|---|
xs[0] | "p" | typed string, though xs[99] is undefined at runtime |
xs[-1] | undefined | reads 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.length | 6 | writable: 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
| Method | Returns | Cost | Copying alternative |
|---|---|---|---|
push(...items) | the new length | O(1) amortized | [...xs, x] |
pop() | the item, or undefined | O(1) | xs.slice(0, -1) |
unshift(...items) | the new length | O(n) | [x, ...xs] |
shift() | the item, or undefined | O(n) by the spec | xs.slice(1); for queues see Stacks, queues & deques |
splice(i, k, ...items) | the removed items | O(n − i) | toSpliced(i, k, ...items) |
xs[i] = x | O(1); past the end it grows the array and leaves holes | xs.with(i, x) | |
concat(...arrays) | a new array | O(n + m) | [...xs, ...ys] |
fill(v, start?, end?) | the same array | O(k) | Array.from({ length }, () => v) |
copyWithin(target, start, end?) | the same array | O(k) | none |
xs.length = n | truncates, or pads with holes | xs.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; // 5Searching & testing
| Method | Returns | When nothing matches | Notes |
|---|---|---|---|
indexOf(x, from?), lastIndexOf | index | -1 | ===: never finds NaN |
includes(x, from?) | boolean | false | SameValueZero: finds NaN |
find(fn), findLast(fn) | T | undefined | undefined | first match from the start or the end |
findIndex(fn), findLastIndex(fn) | index | -1 | |
some(fn) | boolean | false | stops at the first true |
every(fn) | boolean | true on [] | stops at the first false |
filter(fn) | a new array | [] | keeps items where fn is truthy |
a hand-written lowerBound | index | xs.length | O(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: vacuousBinary search
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 end1/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); // 3000000000000ok must be monotonic: false up to some point, then true from there on.
Sorting & reordering
| Call | Returns | Mutates | Notes |
|---|---|---|---|
xs.sort(cmp?) | the same array | yes | without cmp, compares items as strings |
xs.toSorted(cmp?) | a new array | no | ES2023; works on readonly arrays |
xs.reverse(), xs.toReversed() | same / new array | yes / no | |
xs.with(i, v) | a new array | no | |
typed.sort(cmp?) | the same typed array | yes | numeric 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].undefineditems go last (holes after them) and are never passed to a compare function. cmp(a, b)returns a negative number to putafirst, positive to putbfirst,0to keep their order.NaNcounts as0, so a singleNaNitem, ora - bon strings, silently leaves the array unsorted.a - bonbigintthrowsTypeError.- 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 "afirst"). 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]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
| Method | Returns | Notes |
|---|---|---|
for (const x of xs) | statement | break, continue, await; visits holes as undefined |
for (const [i, x] of xs.entries()) | statement | index and item |
forEach(fn) | undefined | no break; ignores returned promises; skips holes |
map(fn) | U[], same length | fn(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 array | flat(Infinity) flattens fully |
keys(), values(), entries() | iterators | keys() includes holes |
join(sep = ",") | string | null 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 copyReducing & grouping
| Method | Returns | Notes |
|---|---|---|
reduce(fn, init) | the accumulator | TypeError on [] without init; type it with reduce<T> |
reduceRight(fn, init) | the accumulator | walks 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); // 10const 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.
| Expression | Gives | Depth |
|---|---|---|
const ys = xs | the same array under a second name | none |
[...xs], xs.slice(), Array.from(xs) | a new array holding the same items | shallow |
structuredClone(xs) | items copied recursively, Maps and Dates included | deep; 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:
| Mutates | Copying 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] = v | with(i, v) |
fill(v) | Array.from({ length }, () => v) |
copyWithin, length = n | none, slice(0, n) |
map, filter, flatMap, flat, slice, concat | already 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; // trueA 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]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
| Operation | Cost | Notes |
|---|---|---|
xs[i], xs[i] = x, xs.length | O(1) | |
push, pop | O(1) amortized | the end is cheap |
shift, unshift | O(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, some | O(n) | a Set answers has in O(1) on average |
slice, spread, concat, toSpliced, with | O(n) | copies |
sort, toSorted | O(n log n) | about O(n) on mostly sorted input |
reverse, fill, map, filter, reduce, join | O(n) | |
xs = [...xs, x] in a loop | O(n²) total | push instead |
a hand-written lowerBound | O(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).
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 arrayDOUBLE; any other value makes itELEMENTS, which stores pointers. - A hole (
new Array(n), writing past the end,delete xs[i],xs.length = bigger) makes the arrayHOLEYfor good, and reading a hole has to check the prototype chain. The one exception, noted by V8 in February 2025:fillover the whole array makes it packed again, sonew Array(n).fill(0)isPACKED_SMI_ELEMENTSon Node 24 and 26 butHOLEY_SMI_ELEMENTSon Node 22. Array.from({ length: n }, fn)and a loop ofpushfrom[]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:
1/5
const xs: number[] = [] has length 0 and an empty backing store.
| Need | Use instead | Why |
|---|---|---|
| fast membership, dedupe | Set | O(1) average has; see Maps & sets |
| lookup by key | Map | O(1) average by key instead of a scan |
| a queue | a head index, or the Deque above | shift is O(n) |
| the smallest item, repeatedly | a binary heap | O(log n) push and pop; see Heaps |
| millions of numbers | Int32Array, Float64Array | 4 or 8 bytes per item, numeric sort, no holes |
| a read-only view for callers | readonly T[] | the compiler removes the mutating methods |
| immutable updates (React state) | toSorted, toSpliced, with, spread | new 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.
| Method | Returns | Notes |
|---|---|---|
Iterator.from(x) | an iterator with helpers | wraps any iterable or iterator |
.map(fn), .filter(fn), .flatMap(fn) | lazy iterators | fn(value, index) |
.take(n), .drop(n) | lazy iterators | make infinite iterators usable |
.toArray() | T[] | consumes the iterator |
.reduce, .some, .every, .find, .forEach | a value | consume, 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);
// 50DS&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.
| Job | TypeScript | Watch out | More |
|---|---|---|---|
| Stack | st.push(x), st.pop(), peek st.at(-1), empty st.length === 0 | pop() on empty returns undefined; if (st.length) tests non-empty | Stacks |
| Queue, BFS | q.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 max | the ring-buffer Deque, or an index array with a head pointer | store indexes, not values, to know when one leaves the window | Monotonic stack & deque |
| Priority queue | a binary heap class: there is no built-in | push then sort on every insert is O(n log n) per insert | Heaps, TS heaps |
| Top k | xs.toSorted(cmp).slice(0, k) | O(n log n); a size-k heap is O(n log k) | Top-k |
| Sorted lookup | lowerBound(xs, x), then i < xs.length && xs[i] === x | indexOf on a sorted array is still O(n) | Lower & upper bound |
| Search the answer | firstTrue(lo, hi, ok) | ok must be monotonic; (lo + hi) >> 1 breaks past 2 ** 31 | On the answer |
| Prefix sums | pre[i + 1] = pre[i] + xs[i] in a pre of length n + 1 | sums past 2 ** 53 lose precision: use bigint | Prefix sums |
| Difference array | diff[l] += v; diff[r + 1] -= v, then a running sum | size it n + 1 so r + 1 exists | Prefix sums |
| Two pointers | let i = 0, j = xs.length - 1; while (i < j) | swap with [xs[i], xs[j]] = [xs[j], xs[i]] | Two pointers |
| DP table | Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)) | new Array(n).fill(new Array(m).fill(0)) shares one row | Bottom-up DP |
| Counting | new Int32Array(26), cnt[s.charCodeAt(i) - 97]++ | arrays are Map keys by identity: key by cnt.join(",") | Group anagrams |
| Sentinels | Infinity, -Infinity | Math.max(...[]) is -Infinity; the spread throws on huge arrays | Numbers |
| Grid neighbors | for (const [dr, dc] of DIRS) with DIRS as const, then bounds checks | grid[-1] is undefined, so grid[-1][c] throws TypeError | Grids as graphs |
| Intervals | ivs.sort((a, b) => a[0] - b[0]) | the default sort compares "10,2" with "9,1" as text | Intervals |
| Argsort | [...xs.keys()].sort((i, j) => xs[i] - xs[j]) | stable, so ties keep index order | Argsort |
| Backtracking | path.push(x), recurse, path.pop(); record out.push([...path]) | out.push(path) stores one array that ends up empty | Backtracking |
| Memo keys | memo.get(`${i},${j}`) or i * cols + j | [i, j] as a Map key never matches a new [i, j] | Maps & sets |
| Deep recursion | an explicit stack: const st = [root]; while (st.length) | Node overflows near 12,500 frames of a tiny function, fewer with more locals | Depth 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
| Type | Meaning |
|---|---|
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, Float64Array | typed 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:
| Write | When |
|---|---|
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] ?? 0 | out 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 list | TypeScript 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] = ys | xs.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 IndexError | xs[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] * n | new 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.deque | a head index, or a ring-buffer Deque |
heapq | no built-in: a Heap class; see Heaps |
tuple | readonly [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); // undefinedMerge 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]); // -1Daily 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
- MDN: Array (opens in a new tab): every method, with browser support
- MDN: Array.prototype.sort (opens in a new tab): default order, compare functions, stability
- MDN: Indexed collections (opens in a new tab): sparse arrays and how each method treats holes
- MDN: TypedArray (opens in a new tab): the typed array types and their conversions
- MDN: Iterator (opens in a new tab): iterator helpers
- MDN: structuredClone (opens in a new tab): deep copies and what can't be cloned
- MDN: Object.groupBy (opens in a new tab) and Intl.Collator (opens in a new tab)
- ECMAScript: Array.prototype.sort (opens in a new tab) and Array.prototype.shift (opens in a new tab): the spec algorithms
- V8 blog: Elements kinds in V8 (opens in a new tab): packed and holey arrays, the lattice, the
fillexception - V8 blog: Getting things sorted in V8 (opens in a new tab): the move to TimSort and a stable sort
- TypeScript handbook: Object types (arrays and tuples) (opens in a new tab)
- TSConfig: noUncheckedIndexedAccess (opens in a new tab)
- TypeScript 5.5 release notes: inferred type predicates (opens in a new tab)