Card 1 / 10
Split arr into groups of size. The last group may be shorter if the array doesn't divide evenly.
arr |
size |
result | exercises |
|---|---|---|---|
[1, 2, 3, 4, 5] |
2 |
[[1, 2], [3, 4], [5]] |
last group is short |
[1, 2, 3] |
1 |
[[1], [2], [3]] |
every element its own group |
[] |
3 |
[] |
empty input |
The return type T[][] is an array of arrays of T โ the groups, not the elements.
arr.slice(start, end) returns a copy of the elements from start up to (but not including) end, leaving arr untouched. Step an index i by size and take arr.slice(i, i + size) each time โ slice clamps a past-the-end end, so the short final group needs no special case.
function chunk<T>(arr: T[], size: number): T[][] {
// Split arr into groups of `size`. The last group may be shorter.
const out: T[][] = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}Card 2 / 10
Flatten arr by exactly one level: elements that are arrays are spread in, other elements are kept as-is. Nesting deeper than one level stays nested.
arr |
result | exercises |
|---|---|---|
[[1, 2], [3], [4, 5]] |
[1, 2, 3, 4, 5] |
array of arrays |
[1, [2, 3], 4] |
[1, 2, 3, 4] |
mixed elements |
[[1, [2]], [3]] |
[1, [2], 3] |
only one level is removed |
[] |
[] |
empty input |
The parameter type (T | T[])[] is an array whose elements are each either a T or an array of T.
Array.isArray(value) returns true only when value is an array โ use it to decide, per element, whether to unpack or to keep.
out.push(...item) spreads item's elements in one by one, while out.push(item) adds it as a single element. (The built-in arr.flat() does exactly this job; writing it out by hand is the point here.)
function flattenOnce<T>(arr: (T | T[])[]): T[] {
// Flatten exactly one level: array elements are spread in, non-array elements kept as-is.
const out: T[] = [];
for (const item of arr) {
if (Array.isArray(item)) out.push(...item);
else out.push(item);
}
return out;
}Card 3 / 10
Remove duplicate values from arr, preserving the first-seen order of each value.
arr |
result | exercises |
|---|---|---|
[1, 2, 2, 3, 1] |
[1, 2, 3] |
repeats dropped, first-seen order kept |
["a", "b", "a"] |
["a", "b"] |
any value type, not just numbers |
[] |
[] |
empty input |
A Set stores each value at most once: seen.has(x) asks whether it is already in there and seen.add(x) puts it in, both without rescanning the array.
Walk arr once and push x onto the result only when seen.has(x) is false, adding it to seen as you go. (A Set iterates in insertion order, so [...new Set(arr)] is a valid one-line alternative โ the explicit loop is here to make the membership check visible.)
function dedupe<T>(arr: T[]): T[] {
// Remove duplicates, preserving the first-seen order of each value.
const seen = new Set<T>();
const out: T[] = [];
for (const x of arr) {
if (!seen.has(x)) {
seen.add(x);
out.push(x);
}
}
return out;
}Card 4 / 10
Rotate arr to the right by k positions and return a new array โ the last k elements move to the front. k may be larger than the array's length.
arr |
k |
result | exercises |
|---|---|---|---|
[1, 2, 3, 4, 5] |
1 |
[5, 1, 2, 3, 4] |
tail element wraps to the front |
[1, 2, 3] |
4 |
[3, 1, 2] |
k larger than the length |
[1, 2, 3] |
0 |
[1, 2, 3] |
no rotation |
[] |
2 |
[] |
empty input |
arr.slice(start, end) copies elements from start up to (but not including) end and leaves arr alone โ unlike splice, which removes them in place. A rotation is just the tail slice followed by the head slice.
Return early when arr is empty (% 0 is NaN), then normalise with const n = ((k % arr.length) + arr.length) % arr.length โ the extra add-and-mod also keeps a negative k in range. Now spread the two slices into one new array: [...arr.slice(arr.length - n), ...arr.slice(0, arr.length - n)].
function rotate<T>(arr: T[], k: number): T[] {
// Rotate arr to the right by k positions. k may exceed the length. Return a new array.
if (arr.length === 0) return [];
const n = ((k % arr.length) + arr.length) % arr.length;
return [...arr.slice(arr.length - n), ...arr.slice(0, arr.length - n)];
}Card 5 / 10
Pair up a and b element by element, stopping at the length of the shorter one.
a |
b |
result | exercises |
|---|---|---|---|
[1, 2, 3] |
["a", "b"] |
[[1, "a"], [2, "b"]] |
extra elements of the longer array are dropped |
[1, 2] |
[3, 4] |
[[1, 3], [2, 4]] |
equal lengths |
[] |
[1] |
[] |
one side empty |
The return type [A, B][] is an array of pairs: each element is a two-slot tuple holding one value from a and one from b.
Math.min(x, y) gives the smaller of two numbers โ take it over the two lengths, then loop by index and push [a[i], b[i]] each step.
function zip<A, B>(a: A[], b: B[]): [A, B][] {
// Pair up elements by index, stopping at the shorter array's length.
const out: [A, B][] = [];
const len = Math.min(a.length, b.length);
for (let i = 0; i < len; i++) out.push([a[i], b[i]]);
return out;
}Card 6 / 10
Return the largest number in arr, or undefined when arr is empty.
arr |
result | exercises |
|---|---|---|
[3, 1, 4, 1, 5] |
5 |
happy path |
[-2, -5, -1] |
-1 |
all negative โ seeding with 0 would be wrong |
[42] |
42 |
single element |
[] |
undefined |
empty input |
Return early for the empty array, then seed a running maximum with arr[0] and compare the rest against it. (Math.max(...arr) looks tempting, but it returns -Infinity rather than undefined for an empty array, and spreading a very large array can overflow the call stack.)
function maxOf(arr: number[]): number | undefined {
// Return the largest number in arr, or undefined if arr is empty.
if (arr.length === 0) return undefined;
let m = arr[0];
for (const x of arr) if (x > m) m = x;
return m;
}Card 7 / 10
Split arr into [pass, fail] using the predicate pred: pass holds the items pred returns true for and fail holds the rest, each keeping the original order.
arr |
pred |
result | exercises |
|---|---|---|---|
[1, 2, 3, 4] |
x => x % 2 === 0 |
[[2, 4], [1, 3]] |
both groups non-empty |
[1, 3] |
x => x > 10 |
[[], [1, 3]] |
nothing passes |
[] |
x => true |
[[], []] |
empty input |
pred: (x: T) => boolean means pred is a function you call with one item that answers true or false; the return type [T[], T[]] is a two-slot tuple of arrays โ always exactly two, never more.
One pass is enough: keep two arrays and send each item to exactly one of them. (pred(x) ? pass : fail).push(x) picks the target array first, then pushes into it.
function partition<T>(arr: T[], pred: (x: T) => boolean): [T[], T[]] {
// Split arr into [pass, fail]: items for which pred is true, then the rest.
const pass: T[] = [];
const fail: T[] = [];
for (const x of arr) (pred(x) ? pass : fail).push(x);
return [pass, fail];
}Card 8 / 10
Return the values present in both a and b, in the order they appear in a, with no duplicates.
a |
b |
result | exercises |
|---|---|---|---|
[1, 2, 3, 4] |
[2, 4, 6] |
[2, 4] |
only shared values survive |
[1, 1, 2] |
[1] |
[1] |
a repeat in a appears once |
[1, 2] |
[3, 4] |
[] |
nothing in common |
new Set(b) collects b's values into a set (duplicates collapse), and setB.has(x) then tests membership in constant time instead of rescanning b for every element of a.
Walk a once with two sets in play: one built from b, and a second seen set of what you have already pushed, so a value repeated in a is emitted only the first time.
function intersection<T>(a: T[], b: T[]): T[] {
// Return values present in both arrays, in the order they appear in a, without duplicates.
const setB = new Set(b);
const seen = new Set<T>();
const out: T[] = [];
for (const x of a) {
if (setB.has(x) && !seen.has(x)) {
seen.add(x);
out.push(x);
}
}
return out;
}Card 9 / 10
Return the elements of a that are not present in b, keeping a's order and its duplicates.
a |
b |
result | exercises |
|---|---|---|---|
[1, 2, 3, 4] |
[2, 4] |
[1, 3] |
shared values removed |
[1, 2, 2, 3] |
[3] |
[1, 2, 2] |
duplicates in a are kept |
[1, 2] |
[1, 2] |
[] |
everything removed |
new Set(b) collects b's values so that setB.has(x) is a constant-time membership test, rather than scanning b again for every element of a.
a.filter(fn) returns a new array of just the elements for which fn returned true โ here, a.filter((x) => !setB.has(x)). Filtering keeps a's order and its duplicates for free.
function difference<T>(a: T[], b: T[]): T[] {
// Return the elements of a that are not present in b, keeping a's order and duplicates.
const setB = new Set(b);
return a.filter((x) => !setB.has(x));
}Card 10 / 10
Return the leading run of elements of arr for which pred is true, stopping at the first element that fails.
arr |
pred |
result | exercises |
|---|---|---|---|
[1, 2, 3, 4, 1] |
x => x < 3 |
[1, 2] |
stops at the first failure, ignoring the later 1 |
[1, 2, 3] |
x => x > 10 |
[] |
the first element already fails |
[1, 2, 3] |
x => true |
[1, 2, 3] |
every element passes |
pred: (x: T) => boolean means pred is a function you call with one item that answers true or false.
This is not filter: filter keeps every matching element wherever it sits, while takeWhile stops for good at the first failure โ which is why the trailing 1 above is left out. Loop and break as soon as pred(x) is false.
function takeWhile<T>(arr: T[], pred: (x: T) => boolean): T[] {
// Return the leading run of elements for which pred is true; stop at the first false.
const out: T[] = [];
for (const x of arr) {
if (!pred(x)) break;
out.push(x);
}
return out;
}