Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 2 – Interview toolkits in five languages

JavaScript and TypeScript toolkit

Map, Set, sorting, numbers, typed arrays and a heap template for coding rounds in JavaScript and TypeScript, with every trap run on Node.js 24.

  • Beginner
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Use Map, Set, typed arrays and modern array methods safely in coding-round solutions
  • Avoid the default-sort, comparator, 32-bit bitwise, number-precision and recursion-depth traps
  • Write a binary heap with a comparator from memory and extend it
  • Run TypeScript on Node.js and check what a judge's TypeScript target accepts

Before you start

What you’ll learn · Before you start

What you will learn

  • Use Map, Set, typed arrays and modern array methods safely in coding-round solutions
  • Avoid the default-sort, comparator, 32-bit bitwise, number-precision and recursion-depth traps
  • Write a binary heap with a comparator from memory and extend it
  • Run TypeScript on Node.js and check what a judge's TypeScript target accepts

Before you start

On this page

JavaScript can solve every coding-round problem, but its standard library gives you less than Python’s or Java’s, and several of its defaults are traps. Map and Set cover hashing, and arrays cover stacks and sorting. There is no heap and no sorted map, numbers are 64-bit floating point, bitwise operators cut values to 32 bits, and sort() compares numbers as text unless you say otherwise. This lesson shows the parts of JavaScript that interview problems use, runs every trap on Node.js 24.21.0 next to its fix, gives you a heap template to write from memory, and covers what changes when you write TypeScript on Node.js or on a judge.

Map and Set before plain objects

An object can count and look things up, but its keys are always strings, so obj[1] and obj["1"] are the same entry. A Map keeps the type of each key, has a size, and iterates in insertion order, which the language specification defines, so output built from it repeats from run to run.

Keys, counting, grouping and set operations JavaScript · maps_and_sets.mjs
// Object keys are always strings, so 1 and "1" are the same key; a Map keeps the type of its keys.
const obj = {};
obj[1] = "number one";
obj["1"] = "text one";
console.log(Object.keys(obj).length, JSON.stringify(obj));
const map = new Map([[1, "number one"], ["1", "text one"]]);
console.log(map.size, map.get(1), "|", map.get("1"));

// Counting with a Map: ?? supplies 0 for a word not seen yet. A Map iterates in insertion order.
const words = "pear fig pear apple fig kiwi apple pear".split(" ");
const counts = new Map();
for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1);
console.log(JSON.stringify([...counts]));

// Grouping in one call (ES2024).
console.log(JSON.stringify(Object.groupBy(words, (w) => `${w.length} letters`)));

// Set operations (ES2025).
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
console.log(JSON.stringify([...a.intersection(b)]), JSON.stringify([...a.union(b)]), JSON.stringify([...a.difference(b)]));

// Arrays are compared by reference, so a Set of pairs needs a key you build, such as "row,col".
const visited = new Set();
visited.add([2, 3]);
console.log(visited.has([2, 3]));
const cells = new Set();
cells.add(`${2},${3}`);
console.log(cells.has(`${2},${3}`));

Output

1 {"1":"text one"}
2 number one | text one
[["pear",3],["fig",2],["apple",2],["kiwi",1]]
{"4 letters":["pear","pear","kiwi","pear"],"3 letters":["fig","fig"],"5 letters":["apple","apple"]}
[2,3] [1,2,3,4] [1]
false
true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node maps_and_sets.mjs

The counting line is the pattern to keep: counts.set(w, (counts.get(w) ?? 0) + 1), where ?? supplies 0 for a key that is not there yet. Object.groupBy (ECMAScript 2024) builds the lists of a “group by” in one call, and Set has had intersection, union and difference since ECMAScript 2025; on an older engine, write them with filter and has.

The last two lines are the trap that costs the most time in grid problems. Arrays are objects, and a Set compares objects by reference, so a new [2, 3] is never “in” the set, even when an equal pair was added. Encode a cell as a string such as "2,3", or as one number, row * cols + col, which is faster to build and compare. The DSA toolkit lesson lists what the specification promises about the cost of these structures.

Sorting: the comparator decides

sort() without a comparator converts the elements to strings and compares those, so [10, 9, 1, 25, 100] comes out as [1, 10, 100, 25, 9]. Pass a comparator for numbers. The specification expects it to return a negative number, zero or a positive number consistently, and when a comparator is not consistent it leaves the resulting order to the engine.

Default sort, a boolean comparator, and two keys JavaScript · sorting.mjs
const nums = [10, 9, 1, 25, 100];

// Without a comparator, sort() converts the numbers to strings and compares those.
console.log(JSON.stringify([...nums].sort()));
console.log(JSON.stringify([...nums].sort((a, b) => a - b)));

// A comparator must return a negative number, zero or a positive number. A boolean is read as 1 or 0, never
// negative, so the engine is told "not smaller" for every pair and the result is not sorted.
const twelve = [5, 1, 4, 2, 8, 3, 9, 7, 6, 12, 11, 10];
console.log(JSON.stringify([...twelve].sort((a, b) => a > b)));
console.log(JSON.stringify([...twelve].sort((a, b) => a - b)));

// Sorting is stable, so equal keys keep their order; || chains a second key.
const people = [["ravi", 31], ["asha", 25], ["meena", 31], ["dev", 25]];
const byAgeThenName = people.toSorted((p, q) => q[1] - p[1] || (p[0] < q[0] ? -1 : p[0] > q[0] ? 1 : 0));
console.log(JSON.stringify(byAgeThenName));
// toSorted (ES2023) returned a new array and left the original alone.
console.log(JSON.stringify(people[0]));

Output

[1,10,100,25,9]
[1,9,10,25,100]
[5,1,4,2,8,3,9,7,6,12,11,10]
[1,2,3,4,5,6,7,8,9,10,11,12]
[["meena",31],["ravi",31],["asha",25],["dev",25]]
["ravi",31]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sorting.mjs

Version note

The Run button executes this file with QuickJS (quickjs-emscripten 0.32.0), whose sort algorithm is not Node.js’s. The boolean comparator gives a different wrong order there, [5,1,2,3,4,7,6,8,9,10,11,12], while every correct comparator gives the same result in both engines.

In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:

[1,10,100,25,9]
[1,9,10,25,100]
[5,1,2,3,4,7,6,8,9,10,11,12]
[1,2,3,4,5,6,7,8,9,10,11,12]
[["meena",31],["ravi",31],["asha",25],["dev",25]]
["ravi",31]

The third line is the comparator (a, b) => a > b. It returns true or false, which the sort reads as 1 or 0, never as a negative number, so the engine is never told that one element comes before another. On Node.js the array came back exactly as it went in; QuickJS, as the note says, produced a different wrong order. A comparator that only works on the sample input is worse than an error, because nothing fails until a hidden test does.

Sorting has been stable by specification since ECMAScript 2019, so elements that compare equal keep their order, and || chains a second key: q[1] - p[1] || byName sorts by age from oldest, then by name. toSorted (ECMAScript 2023) returns a sorted copy and leaves the original array alone, which matters when the caller’s array must not change.

Numbers: doubles, 32-bit bitwise operators and BigInt

Every Number is an IEEE 754 double. Integers are exact up to Number.MAX_SAFE_INTEGER, 2⁵³ − 1, and bitwise operators first convert their operands to 32-bit integers. Several common shortcuts break because of these rules.

32-bit operators, remainders and a product modulo 10⁹ + 7 JavaScript · numbers.mjs
// Bitwise operators work on 32-bit signed integers.
console.log(1 << 31, (1 << 31) >>> 0);

// So "x | 0" and "~~x" truncate only while x fits in 32 bits; Math.trunc works on any Number.
const big = 2 ** 32 + 5.75;
console.log(big | 0, ~~big, Math.trunc(big));

// % keeps the sign of the left operand; add m once more for a remainder in 0 .. m - 1.
const m = 5;
console.log(-17 % m, ((-17 % m) + m) % m);

// A product of two values below 1,000,000,007 can pass 2^53, where Numbers stop being exact.
const MOD = 1_000_000_007;
const x = 999_999_999;
const y = 999_999_998;
console.log((x * y) % MOD, Number((BigInt(x) * BigInt(y)) % BigInt(MOD)));

// Splitting one factor into 15-bit halves keeps every partial product below 2^45, so plain Numbers stay exact.
function mulMod(a, b, mod) {
  const hi = Math.floor(b / 32768);
  const lo = b % 32768;
  return ((((a * hi) % mod) * 32768) % mod + ((a * lo) % mod)) % mod;
}
console.log(mulMod(x, y, MOD));

// Mixing BigInt and Number throws (each engine words the message its own way).
try {
  console.log(1n + 1);
} catch (error) {
  console.log(error.name);
}

Output

-2147483648 2147483648
5 5 4294967301
-2 3
70 72
72
TypeError

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node numbers.mjs

  • 1 << 31 is negative. The result is a signed 32-bit integer, -2147483648; >>> 0 reads the same bits as unsigned. Bit tricks on masks of more than 31 bits need BigInt or two numbers.
  • x | 0 and ~~x truncate only small numbers. Above 2³¹ they wrap around: 4,294,967,301.75 became 5. Use Math.trunc or Math.floor.
  • % keeps the sign of the left operand, so -17 % 5 is -2. For a remainder from 0 to m − 1, write ((x % m) + m) % m.
  • A product modulo 10⁹ + 7 is wrong with plain Numbers. 999,999,999 × 999,999,998 is about 10¹⁸, far past 2⁵³, so the product is rounded before % sees it: 70 instead of 72. BigInt gives the exact 72. So does mulMod, which splits one factor into two 15-bit halves so that no partial product passes 2⁴⁵ and every step stays exact in a Number.

Mixing BigInt and Number in one expression throws a TypeError, so convert every operand. The message itself differs between engines, which is why the example prints only the error’s name.

A common piece of advice says that BigInt is too slow for a loop of modular products and that the split is the fast way. Measured on Node.js 24, it is the other way round:

A million modular products: BigInt against split Numbers JavaScript · mulmod_speed.mjs
// A million products modulo 1,000,000,007, chained so that each one uses the last: with BigInt, and with
// plain Numbers split into 15-bit halves. Both must end on the same value.
const MOD = 1_000_000_007;
const N = 1_000_000;

function mulMod(a, b, mod) {
  const hi = Math.floor(b / 32768);
  const lo = b % 32768;
  return ((((a * hi) % mod) * 32768) % mod + ((a * lo) % mod)) % mod;
}

let start = performance.now();
let big = 123_456_789n;
const bigMod = BigInt(MOD);
for (let i = 0; i < N; i++) big = (big * 987_654_321n) % bigMod;
const bigMs = performance.now() - start;

start = performance.now();
let split = 123_456_789;
for (let i = 0; i < N; i++) split = mulMod(split, 987_654_321, MOD);
const splitMs = performance.now() - start;

console.log(`BigInt:          ${String(big)}, ${Math.round(bigMs)} ms`);
console.log(`split Numbers:   ${split}, ${Math.round(splitMs)} ms`);
console.log(`same result: ${String(big) === String(split)}`);

Output

BigInt:          239439686, 15 ms
split Numbers:   239439686, 603 ms
same result: true

This output changes from run to run: the times depend on the computer and on what else it is running

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node mulmod_speed.mjs

Both chains end on the same value, and BigInt took a small fraction of the split version’s time, because each mulMod call performs four floating-point remainders. Use BigInt for modular products, converting once on the way in and once on the way out, and keep the split for code that must stay in Numbers, such as values stored in a typed array.

Arrays: shared rows, typed arrays and queues

Three array habits decide whether a solution is right and fast enough.

fill() with an object, typed arrays and their wrap-around JavaScript · arrays.mjs
// fill() puts the same object in every slot: three references to one array.
const shared = Array(3).fill([]);
shared[0].push("x");
console.log(JSON.stringify(shared));

// Array.from calls the function once per slot, so every row is a new array.
const rows = Array.from({ length: 3 }, () => []);
rows[0].push("x");
console.log(JSON.stringify(rows));

// A typed array starts filled with zeros and has a fixed size, good for DP tables and counts.
const dp = new Int32Array(5);
dp[4] = 7;
console.log(JSON.stringify([...dp]));

// But it stores every value in its type: 2^31 does not fit in an Int32Array and wraps around, and a
// Uint8Array keeps only the lowest 8 bits.
dp[0] = 2 ** 31;
console.log(dp[0], new Uint8Array([300])[0]);

Output

[["x"],["x"],["x"]]
[["x"],[],[]]
[0,0,0,0,7]
-2147483648 44

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node arrays.mjs

Array(3).fill([]) puts one array into all three slots, so a push to one row shows in every row: MDN’s page on fill warns that an object value makes every slot reference the same object. Array.from({ length: n }, () => []) calls the function once per slot. Typed arrays such as Int32Array start filled with zeros and are compact, which suits DP tables and counters, but they store each value in their own type: 2³¹ does not fit in an Int32Array and wraps to -2147483648, and a Uint8Array keeps only the lowest 8 bits of 300, which is 44. Use a Float64Array or a plain array when sums can grow.

Queues are the third habit. JavaScript has no deque, and shift() moves every remaining element down by one place:

Draining 50,000 items with shift() and with a head index JavaScript · queue_cost.mjs
// Draining a queue of 50,000 items: shift() moves every remaining item down one place on each call,
// while a head index only moves a number.
const n = 50_000;

function drainWithShift() {
  const queue = Array.from({ length: n }, (_, i) => i);
  let total = 0;
  while (queue.length > 0) total += queue.shift();
  return total;
}

function drainWithHead() {
  const queue = Array.from({ length: n }, (_, i) => i);
  let total = 0;
  for (let head = 0; head < queue.length; head++) total += queue[head];
  return total;
}

for (const drain of [drainWithShift, drainWithHead]) {
  const start = performance.now();
  const total = drain();
  console.log(`${drain.name}: total ${total}, ${Math.round(performance.now() - start)} ms`);
}

Output

drainWithShift: total 1249975000, 773 ms
drainWithHead: total 1249975000, 3 ms

This output changes from run to run: the times depend on the computer and on what else it is running

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node queue_cost.mjs

Draining the queue with shift() takes time that grows with the square of its length, so the two lines differ by a factor in the hundreds. A breadth-first search over 10⁵ nodes whose queue grows long can time out this way. Keep the array and an index of its front instead: read queue[head++] and never remove anything. Memory grows to the total number of pushes, which a search uses anyway.

A heap you can write from memory

There is no priority queue in JavaScript, and Dijkstra’s algorithm, top-k problems and k-way merges all need one. This template is short enough to write in five minutes: an array, a comparator, a loop that moves a new item up, and a loop that moves the last item down after a pop.

A min-heap with a comparator, checked against sorting JavaScript · min_heap.mjs
// A binary min-heap with a comparator: the template to keep for coding rounds in JavaScript.
class MinHeap {
  constructor(compare = (a, b) => a - b) {
    this.items = [];
    this.compare = compare;
  }

  get size() {
    return this.items.length;
  }

  peek() {
    return this.items[0];
  }

  push(value) {
    const a = this.items;
    a.push(value);
    let i = a.length - 1;
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (this.compare(a[i], a[parent]) >= 0) break;
      [a[i], a[parent]] = [a[parent], a[i]];
      i = parent;
    }
  }

  pop() {
    const a = this.items;
    if (a.length === 0) return undefined;
    const top = a[0];
    const last = a.pop();
    if (a.length > 0) {
      a[0] = last;
      let i = 0;
      for (;;) {
        const left = 2 * i + 1;
        const right = left + 1;
        let best = i;
        if (left < a.length && this.compare(a[left], a[best]) < 0) best = left;
        if (right < a.length && this.compare(a[right], a[best]) < 0) best = right;
        if (best === i) break;
        [a[i], a[best]] = [a[best], a[i]];
        i = best;
      }
    }
    return top;
  }
}

const drain = (heap) => {
  const out = [];
  while (heap.size) out.push(heap.pop());
  return out;
};

const numbers = new MinHeap();
for (const x of [5, 1, 4, 1, 3]) numbers.push(x);
console.log(JSON.stringify(drain(numbers)));

// Largest first: reverse the comparator.
const largest = new MinHeap((a, b) => b - a);
for (const x of [5, 1, 4, 1, 3]) largest.push(x);
console.log(JSON.stringify(drain(largest)));

// Records: earliest day first, then name from A to Z.
const byName = (p, q) => (p.name < q.name ? -1 : p.name > q.name ? 1 : 0);
const tasks = new MinHeap((p, q) => p.day - q.day || byName(p, q));
tasks.push({ name: "report", day: 3 });
tasks.push({ name: "invoice", day: 3 });
tasks.push({ name: "slides", day: 1 });
console.log(drain(tasks).map((t) => t.name).join(", "));

// Checked against sorting on 1,000 random arrays, with a seeded generator so every run is the same.
function mulberry32(seed) {
  return () => {
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
const random = mulberry32(42);
let agree = 0;
for (let trial = 0; trial < 1000; trial++) {
  const values = Array.from({ length: Math.floor(random() * 40) }, () => Math.floor(random() * 100) - 50);
  const heap = new MinHeap();
  for (const v of values) heap.push(v);
  if (JSON.stringify(drain(heap)) === JSON.stringify(values.toSorted((a, b) => a - b))) agree++;
}
console.log(`heap order equals sorted order on ${agree} of 1000 random arrays`);
console.log("pop on an empty heap:", String(new MinHeap().pop()));

Output

[1,1,3,4,5]
[5,4,3,1,1]
slides, invoice, report
heap order equals sorted order on 1000 of 1000 random arrays
pop on an empty heap: undefined

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node min_heap.mjs

The comparator makes the template general: (a, b) => b - a turns it into a max-heap, and a comparator on records orders tasks by day and then by name. The last lines check the heap the way to check any data structure you write under pressure: against something simpler that is surely right. On 1,000 random arrays from a seeded generator, the heap’s order equals the sorted order every time, and popping an empty heap returns undefined instead of throwing.

Two details are easy to get wrong when you write it from memory. The parent of index i is (i - 1) >> 1, and the children are 2i + 1 and 2i + 2. And in pop(), the last item must be moved to the root only if the heap is not now empty; otherwise popping the only item puts it straight back.

Recursion depth and the iterative rewrite

Deep inputs, such as a path-shaped tree of 10⁵ nodes, are a standard hidden test, and a JavaScript engine’s call stack is small.

How deep a recursion goes, and the same walk with a stack JavaScript · depth.mjs
// How deep can a trivial function recurse before the engine stops it?
let calls = 0;
function dive() {
  calls++;
  dive();
}
try {
  dive();
} catch (error) {
  console.log(`${error.name} after about ${Math.round(calls / 100) * 100} calls`);
}

// A path of 100,000 nodes, as a deep test case of a tree or graph problem would give.
const n = 100_000;
const next = Array.from({ length: n }, (_, i) => (i + 1 < n ? i + 1 : -1));

function countRecursive(node) {
  return node === -1 ? 0 : 1 + countRecursive(next[node]);
}
try {
  console.log("recursive:", countRecursive(0));
} catch (error) {
  console.log("recursive:", error.name);
}

// The same walk with an explicit stack: an array holds what the call stack held.
function countIterative(start) {
  const stack = [start];
  let count = 0;
  while (stack.length > 0) {
    const node = stack.pop();
    if (node === -1) continue;
    count++;
    stack.push(next[node]);
  }
  return count;
}
console.log("iterative:", countIterative(0));

Output

RangeError after about 10400 calls
recursive: RangeError
iterative: 100000

This output changes from run to run: the depth at which the engine stops depends on its stack size and on the machine

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node depth.mjs

On Node.js 24.21.0, a trivial function stopped with a RangeError after about 10,400 calls, so the recursive walk over 100,000 nodes failed. The iterative version keeps the nodes still to visit in an array and has no such limit. Write depth-first searches iteratively in JavaScript when the input can be deep. The in-browser runner of this site has an even smaller stack, which is why this example shows recorded output only.

TypeScript on Node.js and on judges

TypeScript’s types disappear before the code runs, so everything above applies to TypeScript unchanged. Node.js 24 runs .ts files directly: type stripping became the default in v23.6.0 and stable in v24.12.0. Its documentation says that Node.js replaces the type syntax with whitespace and performs no type checking.

Four TypeScript files

TypeScript (top_words.ts) · top_words.ts

type Pair = [word: string, count: number];

function topWords(words: string[], k: number): string[] {
  const counts = new Map<string, number>();
  for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1);
  const pairs: Pair[] = [...counts];
  pairs.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
  return pairs.slice(0, k).map(([w]) => w);
}

console.log(topWords("pear fig pear apple fig kiwi apple pear".split(" "), 2).join(" "));

TypeScript (no_type_check.ts) · no_type_check.ts

// tsc rejects the first line: a string is not a number. Node.js removes the annotation and runs it anyway.
const limit: number = "10";
console.log(limit + 5);

TypeScript (colour_enum.ts) · colour_enum.ts

enum Colour {
  White,
  Grey,
  Black,
}

console.log(Colour.Grey);

TypeScript (colour_const.ts) · colour_const.ts

// An object with "as const" and a type made from it: plain JavaScript once the types are removed.
const Colour = { White: 0, Grey: 1, Black: 2 } as const;
type Colour = (typeof Colour)[keyof typeof Colour];

const state: Colour = Colour.Grey;
console.log(state);
Each file run with Node.js 24.21.0 JavaScript · ts_session.mjs
// Runs Node.js on each TypeScript file, as you would in a terminal, and prints what each run printed.
import { spawnSync } from "node:child_process";

for (const file of ["top_words.ts", "no_type_check.ts", "colour_enum.ts", "colour_const.ts"]) {
  console.log(`$ node ${file}`);
  const run = spawnSync(process.execPath, [file], { encoding: "utf8" });
  if (run.stdout) process.stdout.write(run.stdout);
  // Only the line that names the error: the rest of the report is Node's own call stack.
  const error = run.stderr.split("\n").find((line) => /Error/.test(line));
  if (error) console.log(error);
  if (run.status !== 0) console.log(`[exit status ${run.status}]`);
}

Output

$ node top_words.ts
pear apple
$ node no_type_check.ts
105
$ node colour_enum.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
[exit status 1]
$ node colour_const.ts
1

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ts_session.mjs

  • Type annotations, tuple types and generics such as Map<string, number> run as they are.
  • A type error does not stop the run. The TypeScript compiler rejects const limit: number = "10", but Node.js printed 105, the string "10" joined with 5. Only a type checker such as tsc --noEmit reports the mistake.
  • Syntax that needs JavaScript to be generated, such as an enum, stops with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. An object with as const and a type derived from it does the same job and strips cleanly.

Judges that compile TypeScript first may reject methods the language target does not list. HackerRank’s execution environment page lists TypeScript 5.6.2 on Node.js v20.15.1 with the target ES2020. With an ES2020 target and no lib setting, the compiler’s built-in declarations are those of ES2020, so toSorted (ES2023) and Object.groupBy (ES2024) are type errors there, and Node.js 20 has no Object.groupBy at all. Keep to sort on a copy and to loops with a Map when you cannot check.

Not affiliated

Based on public information about HackerRank. MySmartCoPilot is not affiliated with HackerRank, and HackerRank’s real environments may differ from what this lesson describes.

Practise: extend the heap

The exercise starts from the heap template above. Add pushPop, replace and a static from that builds a heap in O(n). The sample tests check each method on small cases and then run 10,000 random operations against a sorted array.

Exercise · Medium · JavaScript

Add pushPop, replace and from to the heap template

heap.mjs holds the lesson's MinHeap template: push, pop, peek and size work. Add the three methods that Python's heapq also offers and that top-k and k-way merge solutions use:

- pushPop(value) pushes value and then pops the smallest item, in one step: when value is not larger than the top (or the heap is empty), it returns value and leaves the heap as it was; otherwise it returns the old top and keeps value in the heap. - replace(value) pops the smallest item and then pushes value, in one step, and returns the item it popped. The returned item may be larger than value. On an empty heap it returns undefined and the heap holds value. - MinHeap.from(values, compare) builds a heap from an array in O(n) by sifting down from the last parent to the root, instead of n pushes. It must not change the array it is given, and compare is optional, as in the constructor.

Each method sifts at most once, so pushPop and replace cost O(log n). The tests compare your heap with a sorted array on 10,000 random operations.

Starter code · heap.mjs

/** A binary min-heap with a comparator (the lesson's template). */
export class MinHeap {
  constructor(compare = (a, b) => a - b) {
    this.items = [];
    this.compare = compare;
  }

  get size() {
    return this.items.length;
  }

  peek() {
    return this.items[0];
  }

  push(value) {
    const a = this.items;
    a.push(value);
    let i = a.length - 1;
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (this.compare(a[i], a[parent]) >= 0) break;
      [a[i], a[parent]] = [a[parent], a[i]];
      i = parent;
    }
  }

  pop() {
    const a = this.items;
    if (a.length === 0) return undefined;
    const top = a[0];
    const last = a.pop();
    if (a.length > 0) {
      a[0] = last;
      let i = 0;
      for (;;) {
        const left = 2 * i + 1;
        const right = left + 1;
        let best = i;
        if (left < a.length && this.compare(a[left], a[best]) < 0) best = left;
        if (right < a.length && this.compare(a[right], a[best]) < 0) best = right;
        if (best === i) break;
        [a[i], a[best]] = [a[best], a[i]];
        i = best;
      }
    }
    return top;
  }

  /** Pushes value, then pops and returns the smallest item. */
  pushPop(value) {
    throw new Error("pushPop is not written yet");
  }

  /** Pops and returns the smallest item (undefined when empty), then pushes value. */
  replace(value) {
    throw new Error("replace is not written yet");
  }

  /** A heap of the values, built in O(n); the array itself is not changed. */
  static from(values, compare) {
    throw new Error("from is not written yet");
  }
}
The sample tests · heap.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { MinHeap } from './heap.mjs';

const drain = (heap) => {
  const out = [];
  while (heap.size) out.push(heap.pop());
  return out;
};

test('pushPop on an empty heap returns the value and keeps the heap empty', () => {
  const h = new MinHeap();
  assert.equal(h.pushPop(4), 4);
  assert.equal(h.size, 0);
});

test('pushPop with a value not larger than the top returns it and changes nothing', () => {
  const h = MinHeap.from([5, 7, 9]);
  assert.equal(h.pushPop(2), 2);
  assert.equal(h.pushPop(5), 5);
  assert.deepEqual(drain(h), [5, 7, 9]);
});

test('pushPop with a larger value returns the old top and keeps the value', () => {
  const h = MinHeap.from([5, 7, 9]);
  assert.equal(h.pushPop(8), 5);
  assert.deepEqual(drain(h), [7, 8, 9]);
});

test('replace returns the old top even when the new value is smaller', () => {
  const h = MinHeap.from([5, 7, 9]);
  assert.equal(h.replace(1), 5);
  assert.deepEqual(drain(h), [1, 7, 9]);
});

test('replace on an empty heap returns undefined and keeps the value', () => {
  const h = new MinHeap();
  assert.equal(h.replace(3), undefined);
  assert.deepEqual(drain(h), [3]);
});

test('from builds a heap that pops in sorted order and leaves the array alone', () => {
  const values = [9, 4, 7, 1, 8, 2, 2, 6];
  const h = MinHeap.from(values);
  assert.equal(h.peek(), 1);
  assert.deepEqual(drain(h), [1, 2, 2, 4, 6, 7, 8, 9]);
  assert.deepEqual(values, [9, 4, 7, 1, 8, 2, 2, 6]);
});

test('from uses the comparator it is given', () => {
  const h = MinHeap.from([3, 10, 1, 7], (a, b) => b - a);
  assert.deepEqual(drain(h), [10, 7, 3, 1]);
});

test('from an empty array gives an empty heap', () => {
  const h = MinHeap.from([]);
  assert.equal(h.size, 0);
  assert.equal(h.pop(), undefined);
});

test('10,000 random operations agree with a sorted array', () => {
  let seed = 7;
  const random = () => {
    seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
    return seed / 4294967296;
  };
  const heap = MinHeap.from([50, 20, 80]);
  const model = [20, 50, 80];
  const insert = (v) => {
    let i = 0;
    while (i < model.length && model[i] <= v) i++;
    model.splice(i, 0, v);
  };
  for (let step = 0; step < 10000; step++) {
    const v = Math.floor(random() * 1000);
    const op = Math.floor(random() * 4);
    if (op === 0) {
      heap.push(v);
      insert(v);
    } else if (op === 1) {
      assert.equal(heap.pop(), model.shift(), `pop at step ${step}`);
    } else if (op === 2) {
      insert(v);
      assert.equal(heap.pushPop(v), model.shift(), `pushPop(${v}) at step ${step}`);
    } else {
      const expected = model.length ? model.shift() : undefined;
      insert(v);
      assert.equal(heap.replace(v), expected, `replace(${v}) at step ${step}`);
    }
    assert.equal(heap.size, model.length, `size at step ${step}`);
  }
});
A hint

All three methods reuse the loop at the end of pop(), which moves the item at one index down until neither child is smaller. Move that loop into a method of its own, #siftDown(i), call it from pop(), and the new methods become a few lines each: replace puts value at index 0 and sifts it down, pushPop does the same only when value is larger than the top, and from sifts down every index from (n >> 1) - 1 to 0.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

JavaScript & TypeScript Online Compiler Try the heap on your own inputs before you run the tests.

Interview questions

Fresher · Why does [10, 9, 1].sort() return [1, 10, 9] in JavaScript, and how do you sort numbers?

Without a comparator, Array.prototype.sort converts each element to a string and compares the strings by their UTF-16 code units, so "10" comes before "9", because the character 1 comes before 9. The numbers are only compared as text; the array still holds numbers afterwards. To sort numbers, pass a comparator that returns a negative number, zero or a positive number: (a, b) => a - b for ascending order and (a, b) => b - a for descending. A comparator that returns a boolean, such as (a, b) => a > b, is a common mistake: it never returns a negative value, so the result depends on the engine and is usually not sorted. Sorting is stable since ECMAScript 2019, and toSorted returns a sorted copy instead of changing the array.

Key takeaways

  • Use Map and Set for hashing; encode pairs and cells as strings or single numbers, because arrays compare by reference.
  • Always pass a numeric comparator to sort, never a boolean one; chain keys with ||.
  • Bitwise operators work on 32 bits, | 0 breaks above 2³¹, % keeps the sign of the left operand, and products modulo 10⁹ + 7 need BigInt or a split multiplication.
  • Array(n).fill([]) shares one array; typed arrays wrap values that do not fit their type.
  • Never use shift() on a long queue: keep a head index. Write deep searches iteratively.
  • Keep a tested heap template; Node.js runs TypeScript without checking types, and a judge’s target decides which methods compile.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What does [5, 40, 100].sort() return?

    Choose one answer.

    Show the answer to question 1

    Answer: [100, 40, 5]

    Without a comparator the numbers are compared as strings: "100" < "40" < "5", because the first characters 1, 4 and 5 decide. Pass (a, b) => a - b to sort them as numbers.

  2. Question 2 of 6 Why is arr.sort((a, b) => a > b) wrong even when it seems to work on a small sample?

    Choose one answer.

    Show the answer to question 2

    Answer: It returns true or false, read as 1 or 0, never a negative number, so the order it produces is up to the engine

    A comparator must say "a comes first" with a negative number. A boolean never does, so the comparator is not consistent and the specification leaves the result to the engine: in the lesson's run, Node.js returned the twelve numbers unchanged and QuickJS returned another wrong order.

  3. Question 3 of 6 For x = 2 ** 32 + 5.75, which expression gives 4294967301?

    Choose one answer.

    Show the answer to question 3

    Answer: Math.trunc(x)

    The bitwise operators convert their operands to 32-bit integers first, so x | 0, ~~x and x >> 0 all wrap around and give 5. Math.trunc works on the whole double and drops only the fraction.

  4. Question 4 of 6 Which line builds 3 rows that can be changed independently?

    Choose one answer.

    Show the answer to question 4

    Answer: Array.from({ length: 3 }, () => [])

    fill puts the same object into every slot, so every variant that ends with fill([]) or fill(new Array()) shares one row. Array.from calls its function once per slot and gets a new array each time.

  5. Question 5 of 6 A breadth-first search in JavaScript keeps its queue in an array. Which way of taking the next node stays fast when the queue holds 10^5 nodes?

    Choose one answer.

    Show the answer to question 5

    Answer: Read queue[head] and then add 1 to head, never removing anything

    shift() and splice(0, 1) move every remaining element, so each step costs O(n). pop() is fast but takes the newest node, which gives a depth-first order. A head index costs O(1) and keeps first-in, first-out order.

  6. Question 6 of 6 Node.js 24 runs a .ts file that declares enum Colour { White, Grey }. What happens?

    Choose one answer.

    Show the answer to question 6

    Answer: It stops with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, because an enum needs generated JavaScript

    Node.js only strips erasable type syntax and checks no types. An enum is not erasable, so the file stops with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, as in the lesson's run. An object with as const and a type made from it strips cleanly.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.