TypeScript Built-In Data Structures for Coding Interviews

May 29, 202610 min read
dsaalgorithmsinterview-prepleetcode
TypeScript Built-In Data Structures for Coding Interviews
TL;DR
  • Map.get() returns T | undefined, not T; always use ?? 0 for frequency counters or you silently propagate NaN through every calculation.
  • Set.has() is O(1), Array.includes() is O(n); swap them anywhere you check membership inside a loop.
  • Record<K,V> for fixed key sets, Map for dynamic keys; numeric-looking string keys in objects sort before string keys, which breaks iteration order.
  • TypeScript has no built-in heap or sorted map; bring a MinHeap class with a comparator to any Dijkstra, top-K, or merge-K problem.
  • Array.sort() is lexicographic by default; always pass (a, b) => a - b for numeric sorts.
  • BigInt is required for multiplications exceeding 2^53 - 1; all numbers are float64, and precision silently fails above that bound.

TypeScript is JavaScript with a safety harness. At runtime they're identical. But the type system changes where bugs surface, and that matters enormously when you're mid-interview and your frequency counter silently turns into a NaN factory. Here's how it happens: Map.get() returns T | undefined. You forget the default value. You add undefined to a number. JavaScript shrugs. TypeScript screams at compile time. In an interview, you want TypeScript screaming.

This covers every structure you'll actually reach for, what each operation costs, and the traps that bite TypeScript candidates specifically.

TypeScript Built-In Data Structures: Quick Reference

StructureAccessInsertDeleteMembershipNotes
Array<T>O(1)O(1) amortized (push) / O(n) (shift)O(1) (pop) / O(n) (splice)O(n)Sort is lexicographic by default
Map<K,V>O(1) avgO(1) avgO(1) avgO(1) avgAny key type; .get() returns T | undefined
Set<T>,O(1) avgO(1) avgO(1) avg.has() beats Array.includes()
Record<K,V> / objectO(1)O(1)O(1)O(1)String keys only; numeric keys sort first

Array: Your Default Container (and Your O(n) Accident Waiting to Happen)

const nums: number[] = [3, 1, 4]; nums.push(5); // O(1) amortized nums.pop(); // O(1) nums.unshift(0); // O(n), shifts everything right nums.shift(); // O(n), shifts everything left nums.splice(1, 1); // O(n)

push and pop are the only O(1) array operations. Everything touching the front is O(n) because JavaScript arrays are contiguous in V8. If you need a FIFO queue with O(1) at both ends, use a head pointer or a different data structure. Do not use shift() in a BFS loop and then wonder why your elegant O(V+E) solution is actually O(V(V+E)).

TypeScript adds tuple types: fixed-length arrays with per-position types.

const point: [number, number] = [3, 7]; const entry: [string, number] = ["count", 42]; const [x, y] = point;

Tuples come up constantly: coordinate pairs, [start, end] intervals, Map.entries() iteration. They're cleaner than {x: number, y: number} when positions are obvious.

For bit manipulation, TypedArrays give you fixed-width integers:

const bits = new Uint32Array(1024); // unsigned 32-bit, zero-initialized const signed = new Int32Array(n);

Elements are stored as actual integers, not float64 boxed values. Useful when you need guaranteed 32-bit wraparound or are simulating a bitset.

Map: Use This Over Plain Objects

Map<K,V> is almost always the right choice for frequency tables, memoization caches, and adjacency lists.

const freq = new Map<string, number>(); freq.set("a", (freq.get("a") ?? 0) + 1); // idiomatic increment for (const [char, count] of freq) { // insertion-order iteration console.log(char, count); } freq.size; // not .length freq.has("a"); // O(1) freq.delete("a"); // O(1)

Map.get() returns T | undefined in TypeScript. This is the most common TypeScript-specific bug in interviews. In JavaScript it silently returns undefined and you only notice when arithmetic gives you NaN. TypeScript surfaces it as a type error with strict null checks on. This is TypeScript being your friend. Accept the gift.

// Wrong: TypeScript flags this in strict mode const count: number = freq.get("a") + 1; // Error: object is possibly undefined // Correct: nullish coalescing const count = (freq.get("a") ?? 0) + 1; // Also correct: non-null assertion after .has() if (freq.has("a")) { const count = freq.get("a")! + 1; }

Use ?? not ||. The || operator skips a legitimate value of 0, which will quietly wreck any problem where zero is a valid frequency. You'll add one too many to your count, spend five minutes staring at a test case that "should work," and then find this line.

Map keys can be any type, compared by identity rather than deep equality.

const nodeMap = new Map<object, number>(); const node = { id: 1 }; nodeMap.set(node, 42); nodeMap.get(node); // 42 nodeMap.get({ id: 1 }); // undefined (different reference)

See hash map time complexity for why O(1) average comes with caveats around hash collisions.

Set.has() Is O(1). Array.includes() Is Not.

const seen = new Set<number>(); seen.add(5); seen.has(5); // O(1) seen.delete(5); // O(1) seen.size; const unique = new Set([1, 2, 2, 3]); // Set {1, 2, 3} const arr = [...unique];

Replace Array.includes() with Set.has() anywhere you check membership in a loop. In graph traversal where you're checking visited nodes on every step, an O(n) membership check turns an O(V+E) algorithm into something that passes no test cases and defeats you spiritually.

function intersection(a: number[], b: number[]): number[] { const setA = new Set(a); return b.filter(x => setA.has(x)); }

Node 22 added native Set.prototype.union(), intersection(), and difference(). LeetCode runs Node 18. These are not available. Build them manually and make peace with it.

Record for Fixed Keys, Map for Dynamic

Record<K, V> is a typed object with string keys.

const memo: Record<string, number> = {}; memo["key"] = 42; type Direction = "up" | "down" | "left" | "right"; const delta: Record<Direction, [number, number]> = { up: [-1, 0], down: [1, 0], left: [0, -1], right: [0, 1], };

Use Record when keys are a fixed, known set. Use Map when keys are dynamic. The Record approach is slightly faster for string keys because V8 can optimize property access on stable-shaped objects, but this rarely matters in a 45-minute interview.

Two traps worth knowing:

Numeric key ordering. Numeric-looking string keys sort ascending first, then string keys follow in insertion order. This is not a bug. It's a feature. That no one wanted.

const obj: Record<string, string> = {}; obj["10"] = "ten"; obj["2"] = "two"; obj["foo"] = "foo"; Object.keys(obj); // ["2", "10", "foo"] (not insertion order)

Map doesn't do this. If key ordering matters, use Map.

The prototype footgun. Writing to keys like "constructor" or "__proto__" shadows built-in methods. Map has no prototype chain, so it's immune.

What WeakMap Does (and Why You Won't Write It in an Interview)

WeakMap<K,V> and WeakSet<T> hold weak references to their object keys. If nothing else references the key, it becomes eligible for garbage collection and the entry disappears.

const cache = new WeakMap<object, number>(); let obj = { id: 1 }; cache.set(obj, 42); obj = null as any; // entry eventually GC'd

Keys must be objects, and the structures are not iterable. No .keys(), no for...of. Only get, set, has, and delete by key. You'll almost never write this in a coding interview, but it's a reasonable system design follow-up about caching strategies and memory management.

What TypeScript Adds (and What It Doesn't)

Type annotations don't change runtime performance. What they change is where bugs surface. TypeScript catches the Map.get() undefined case at compile time instead of giving you a NaN result you have to trace through five function calls. It forces null-handling to be explicit, which means edge cases get flagged before you run the code.

Readonly types are worth knowing for interview use:

function process(items: readonly number[]): void { // items.push(4); // TS error: intentional } const DIRS: readonly [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];

What TypeScript Is Missing (You'll Feel Every Single One)

There is no built-in heap. For any top-K, Dijkstra, or merge-K-lists problem, you bring your own. All 40 lines of it. Into a timed interview. Memorize the shape and stop mourning Java's PriorityQueue.

class MinHeap<T> { private data: T[] = []; constructor(private cmp: (a: T, b: T) => number) {} push(val: T): void { this.data.push(val); let i = this.data.length - 1; while (i > 0) { const p = (i - 1) >> 1; if (this.cmp(this.data[i], this.data[p]) < 0) { [this.data[i], this.data[p]] = [this.data[p], this.data[i]]; i = p; } else break; } } pop(): T | undefined { if (!this.data.length) return undefined; const top = this.data[0]; const last = this.data.pop()!; if (this.data.length) { this.data[0] = last; let i = 0; while (true) { const l = 2*i+1, r = 2*i+2; let s = i; if (l < this.data.length && this.cmp(this.data[l], this.data[s]) < 0) s = l; if (r < this.data.length && this.cmp(this.data[r], this.data[s]) < 0) s = r; if (s === i) break; [this.data[i], this.data[s]] = [this.data[s], this.data[i]]; i = s; } } return top; } peek(): T | undefined { return this.data[0]; } get size(): number { return this.data.length; } } const heap = new MinHeap<number>((a, b) => a - b); heap.push(3); heap.push(1); heap.push(2); heap.pop(); // 1

See the heap data structure for why the index math works.

There is also no sorted map. For problems needing ordered iteration with O(log n) insert (Java's TreeMap, C++'s std::map), your options are a sorted array with binary search, or rethinking the algorithm entirely.

Five Gotchas That Bite TypeScript Candidates

1. Array.sort() is lexicographic by default.

[10, 2, 1, 20].sort(); // [1, 10, 2, 20] wrong [10, 2, 1, 20].sort((a, b) => a - b); // [1, 2, 10, 20] correct

TypeScript doesn't warn about this. JavaScript invented this behavior in 1995 and we all agreed to live with it. Always pass a comparator for numeric sorts, every single time, without exception.

2. Map.get() returns T | undefined, not T.

(map.get(key) ?? 0) is the correct idiom for frequency counters. Forgetting the default causes NaN to propagate silently through every downstream calculation. TypeScript will catch it in strict mode. LeetCode, which runs without strict mode, absolutely will not.

3. Numeric string keys in objects sort before string keys.

Covered in the Record section. If iteration order matters, use Map.

4. NaN !== NaN, but Set and includes() handle it. indexOf() does not.

NaN === NaN; // false [NaN].includes(NaN); // true (SameValueZero) [NaN].indexOf(NaN); // -1 (uses ===) new Set([NaN]).has(NaN); // true

Set.has() and Array.includes() treat NaN as equal to itself. Array.indexOf() does not. This rarely comes up, but it's exactly the kind of edge case interviewers love asking about after you say "I know JavaScript pretty well."

5. No integer type. Everything is float64.

Number.MAX_SAFE_INTEGER; // 2^53 - 1 = 9007199254740991 2 ** 53 === 2 ** 53 + 1; // true (precision lost) const big = BigInt(2) ** BigInt(63); // use BigInt for large multiplications

Problems involving large multiplications require either BigInt or modular arithmetic keeping intermediate values under 2^53. This trips up candidates who assume JavaScript integers work like Java long. They do not. They are all floats. Everything is floats. It's floats all the way down.


For a broader comparison of interview languages, see JavaScript for Coding Interviews. If you want to practice applying these patterns under real interview conditions, SpaceComplexity runs voice-based mock interviews with rubric feedback on exactly this kind of applied fluency.

Further Reading