Last Updated: March 31, 2026
This chapter is a focused crash course on the TypeScript features you will use repeatedly while learning DSA and preparing for coding interviews. Instead of covering the entire language, we will concentrate only on the parts that matter for solving problems efficiently in interviews.
TypeScript on LeetCode works like JavaScript with types bolted on. You do not need to import anything for the core data structures and utilities. Everything is globally available:
| Built-in | What It Provides | DSA Use Case |
|---|---|---|
Array | Dynamic array with rich methods | Primary data structure for everything |
Map | Hash map with any key type | Frequency counting, O(1) lookups |
Set | Hash set with any value type | Visited tracking, deduplication |
Math | max, min, floor, ceil, abs, sqrt, log2 | Utility calculations |
JSON | stringify, parse | Deep copy, composite keys |
Infinity / -Infinity | Global sentinel values | Min/max initialization |
But TypeScript (and JavaScript) also has notable gaps. These are data structures you will need in DSA that have no built-in equivalent:
| Not Built-in | Java / Python Equivalent | What You Must Do |
|---|---|---|
| Queue | Queue / deque | Use object with pointers, or linked list |
| Deque | ArrayDeque / deque | Use array (limited), or custom class |
| PriorityQueue | PriorityQueue / heapq | Implement a min-heap yourself |
| TreeMap / TreeSet | TreeMap / SortedList | Sorted array + binary search (limited) |
We will cover typed implementations for each of these missing structures later in this chapter. For now, know that TypeScript gives you less out of the box than Java or Python, but what it does give you is clean, type-safe, and fast to write.
TypeScript is a statically typed superset of JavaScript. You declare types explicitly, or let the compiler infer them:
For variable declarations, follow one rule: use `const` by default, `let` when you need to reassign, never `var`. The var keyword has function scope (not block scope), which leads to subtle bugs in loops and conditionals. Both let and const are block-scoped, which is what you expect.
For DSA, you will use a small set of types:
| Type | Example | DSA Use Case |
|---|---|---|
number | 42, 3.14, Infinity | Everything numeric (no separate int/float!) |
string | "hello" | Immutable character sequences |
boolean | true, false | Visited arrays, flags |
null | null | Intentional absence (tree/linked list terminators) |
undefined | undefined | Uninitialized variables, missing properties |
bigint | 42n | Values exceeding 2^53 - 1 (rare in DSA) |
Now here is the most important thing to understand about TypeScript numbers: there is no integer type. Every number is an IEEE 754 64-bit floating-point double, exactly like JavaScript. The number 42 and the number 42.0 are the same value, the same type, stored the same way.
This has three critical implications for DSA:
1. Division always produces a float. Unlike Java where 7 / 2 gives 3, in TypeScript 7 / 2 gives 3.5. You must explicitly truncate:
2. Integer precision is limited to 2^53 - 1. The Number.MAX_SAFE_INTEGER is 9,007,199,254,740,991. Beyond this, integer arithmetic silently loses precision:
For most DSA problems where constraints are up to 10^9, this is not an issue. If you truly need arbitrary precision, use BigInt:
3. Binary search midpoint does not overflow, but needs floor. Unlike Java where (left + right) can overflow, TypeScript numbers safely hold sums up to 2^53. But you need Math.floor because division produces a float:
The ternary operator works exactly as in other languages:
TypeScript's type system is its biggest advantage over JavaScript for DSA. The compiler catches bugs before you run your code. Here is what you need to know.
When to annotate vs. let TypeScript infer:
Function signatures should always be annotated. This is where type safety pays off most:
Notice the ! after map.get(complement). This is the non-null assertion operator. We already checked map.has(complement), so we know the value exists, but TypeScript does not know that. The ! tells the compiler "trust me, this is not undefined." We will cover this in detail in the Null Handling section.
Array types have two equivalent syntaxes:
Use number[] for simple types and Array<T> when the element type is complex (like Array<[number, number]>).
Tuple types are one of TypeScript's best features for DSA. Unlike plain arrays, tuples have fixed length and typed positions:
Union types handle cases where a value can be one of several types:
Type narrowing is how TypeScript automatically refines a union type based on control flow:
The compiler tracks which branches have already handled null, so you get full type safety without extra casts.
Most operators work exactly like JavaScript, but a few deserve special attention for DSA.
Always use `===` and `!==` in TypeScript. The == operator performs type coercion, which creates surprising results:
TypeScript's type system helps here. It will flag many invalid == comparisons as errors. But === is still the right habit.
The % operator appears in hashing, cyclic array problems, and math-heavy questions. Like JavaScript, TypeScript's % can return negative values for negative operands: -7 % 3 gives -1, not 2. To guarantee a positive result:
Many problems ask you to return the result "modulo 10^9 + 7":
Here is a critical gotcha: bitwise operators convert operands to 32-bit signed integers. This means they work correctly for values up to about 2 billion, but silently produce wrong results for larger numbers:
The key bitwise operators:
| Operator | Symbol | DSA Use |
|---|---|---|
| AND | & | Masking, checking if bit is set |
| OR | | | Setting bits |
| XOR | ^ | Finding unique elements, toggling |
| NOT | ~ | Bit inversion |
| Left shift | << | Multiply by powers of 2: 1 << n equals 2^n |
| Right shift | >> | Signed right shift (preserves sign) |
| Unsigned right shift | >>> | Unsigned right shift (fills with 0) |
Warning: 1 << 31 gives -2147483648 (negative!) because it sets the sign bit of a 32-bit integer. For bitmask problems with up to 20-25 elements, this is not a concern. For problems using all 32 bits, be careful.
TypeScript offers three loop forms. Use each where it fits:
One limitation: the for-of loop does not give you the index. If you need both index and value, use for with an index or nums.forEach((val, idx) => ...).
These two operators are essential for safe access in TypeScript:
The ?? operator is different from ||. The || operator treats 0, "", and false as falsy and returns the right side. The ?? operator only triggers on null and undefined. For frequency counting where 0 is a valid count, always use ??.
In DSA problems on LeetCode, your solution is typically a function or a class method. TypeScript supports both regular functions and arrow functions:
TypeScript, like JavaScript, is always pass-by-value. But for objects (including arrays, Maps, Sets), the "value" is a reference. This means:
arr[i] = 5), but reassigning the reference itself does not affect the caller.This is why backtracking works. You modify a shared array and undo the modification after recursion:
Notice the [...path] spread when saving results. If you wrote results.push(path) instead, every entry in results would be a reference to the same array, and they would all end up empty after backtracking unwinds.
Generics let you write type-safe, reusable code. They are one of TypeScript's best features for DSA data structures:
You will see generics heavily used in the Queue and MinHeap implementations later. For now, think of <T> as "this works for any type T."
Closures capture variables from their enclosing scope. This is useful in DSA for creating comparators and memoization:
Arrays are the most fundamental data structure and the starting point for almost every DSA problem.
Array initialization matters. There is a gotcha with 2D arrays:
This is one of the most common bugs in matrix problems. fill() does not clone objects. It fills every slot with the same reference.
The `.length` property gives the array size. Unlike Java where arrays, strings, and lists all use different syntax, TypeScript is consistent:
2D arrays appear in every matrix problem (BFS on grid, dynamic programming tables):
For DP problems, you often need a table with one extra row and column for the base case:
The 4-directional neighbor pattern is one of the most common patterns in matrix problems:
For 8-directional movement (including diagonals), add the four diagonal pairs: [-1,-1], [-1,1], [1,-1], [1,1].
The spread operator ... is invaluable for copying and combining arrays:
`Array.from()` creates arrays from iterables or with initialization logic:
Strings in TypeScript are immutable, just like Java and Python. Every time you "modify" a string, you create a new one. This has a critical performance implication:
TypeScript does not have a StringBuilder class like Java. The standard approach is to collect parts in an array and join at the end.
Here are the string methods you will use most often:
| Method | Returns | Example | DSA Use |
|---|---|---|---|
charAt(i) or [i] | string | s[0] | Access individual characters |
length | number | s.length | Loop bounds (property, no parentheses) |
substring(start, end) | string | s.substring(0, 3) | Extract portions (end exclusive) |
slice(start, end) | string | s.slice(0, 3) | Same as substring, supports negative indices |
split(sep) | string[] | s.split(" ") | Tokenize strings |
indexOf(str) | number | s.indexOf("ab") | Find substrings (-1 if not found) |
includes(str) | boolean | s.includes("ab") | Substring check |
startsWith(prefix) | boolean | s.startsWith("ab") | Prefix check |
endsWith(suffix) | boolean | s.endsWith("ab") | Suffix check |
trim() | string | s.trim() | Remove leading/trailing whitespace |
toLowerCase() | string | s.toLowerCase() | Case conversion |
toUpperCase() | string | s.toUpperCase() | Case conversion |
repeat(n) | string | "ab".repeat(3) | "ababab" |
Template literals let you embed expressions in strings, which is useful for creating composite Map keys:
TypeScript does not have a separate char type. Characters are single-character strings. To work with character codes, use charCodeAt and String.fromCharCode:
The frequency array pattern using character codes:
This is faster and more memory-efficient than Map<string, number> when you know the character set is limited to lowercase (or uppercase, or digits).
This is the most important section of this chapter. Choosing the right collection is often the difference between an O(n) and an O(n^2) solution. TypeScript inherits JavaScript's collections but adds type safety through generics.
ES6 Map with TypeScript generics gives you a type-safe hash map with O(1) average-case operations:
A critical type safety point: map.get(key) returns V | undefined in TypeScript. This forces you to handle the missing-key case, unlike Java where you might silently unbox null to int and crash:
Important: Sets use reference equality for objects and arrays. new Set([[1,2], [1,2]]) has size 2, not 1, because the two [1,2] arrays are different objects. For composite keys, serialize to strings:
TypeScript's Record type creates typed plain objects, useful as lightweight maps with string keys:
Use Map<K, V> as your default. Use Record only when you need a quick string-keyed frequency map and want concise code.
TypeScript has no built-in queue with O(1) dequeue. Here is a typed implementation using an object as a sparse array with head/tail pointers:
BFS using this queue:
For level-order traversal, capture the queue size at the start of each level:
TypeScript has no built-in priority queue. For problems like top-K elements, Dijkstra's algorithm, or merge-K-sorted-lists, you need a min-heap. Here is a generic typed implementation:
Usage:
TypeScript has no sorted map or sorted set. This is a genuine disadvantage for problems that need:
Workarounds:
| Collection | Type | Key Methods | Time Complexity | DSA Use |
|---|---|---|---|---|
Array<T> | Dynamic array | push, pop, shift, splice, slice, sort | push/pop O(1), shift O(n), access O(1) | Stacks, results, DP tables |
Map<K,V> | Hash map | set, get, has, delete, size | O(1) average | Frequency counting, two-sum |
Set<T> | Hash set | add, has, delete, size | O(1) average | Visited tracking, dedup |
Record<K,V> | Typed object | bracket access, in, Object.keys | O(1) average | Simple frequency maps |
Queue<T> (custom) | FIFO queue | enqueue, dequeue, peek, size | O(1) all | BFS |
MinHeap<T> (custom) | Priority queue | push, pop, peek, size | O(log n) push/pop | Top-K, Dijkstra |
Sorting is a prerequisite for many algorithms: binary search, two pointers on sorted arrays, merge intervals, and greedy approaches.
The biggest JavaScript/TypeScript sorting gotcha: Array.sort() converts elements to strings and sorts lexicographically by default:
Note that Array.sort() modifies the array in place and returns the same array. If you need to preserve the original, spread first:
Important: TypeScript's sort() is not guaranteed to be stable in all engines, but in practice V8 (Node.js, Chrome) and SpiderMonkey (Firefox) both use TimSort, which is stable. On LeetCode, you can rely on stability.
TypeScript inherits JavaScript's functional array methods. These make DSA code concise, but use them with awareness of their performance characteristics:
Performance note: Each of these methods creates a new array (except forEach, some, every, and find). Chaining .filter().map() creates two intermediate arrays. In hot inner loops processing millions of elements, a plain for loop is faster. For most DSA problems, the difference is negligible.
TypeScript distinguishes between null and undefined, and its type system helps you handle both safely.
The difference:
undefined means "not assigned." It is the default value for uninitialized variables, missing object properties, and Map.get() for missing keys.null means "intentionally absent." You use it explicitly to represent "no node" in trees and linked lists.In DSA, you encounter null/undefined primarily in three situations: tree/linked list problems, map lookups, and optional function parameters.
Rule 1: Use union types to express nullability.
Rule 2: Use nullish coalescing (`??`) for safe defaults.
Rule 3: Use the non-null assertion (`!`) sparingly and only when you are certain.
Rule 4: Use optional chaining (`?.`) for safe property access.
Unlike Java's ConcurrentModificationException, TypeScript does not throw errors when you modify a collection during iteration. But that does not mean it is safe. The behavior is simply undefined and can produce bugs that are hard to track down.
Safe alternatives:
For Maps and Sets, deleting during for-of iteration is actually safe per the ES6 specification. But adding new elements during iteration may or may not be visited, so avoid it:
In practice, most DSA problems do not require removing during iteration. You are far more likely to build a new collection with the desired elements.
Recursion is the backbone of tree traversal, graph DFS, backtracking, and divide-and-conquer. Understanding how TypeScript handles recursion is essential.
Every function call goes on the call stack, which has limited space. If your recursion goes too deep, you get a RangeError: Maximum call stack size exceeded:
When to worry about stack depth:
| Scenario | Typical Depth | Risk |
|---|---|---|
| Balanced binary tree (n nodes) | O(log n) | Safe for n up to 10^6 |
| Linked list / skewed tree (n nodes) | O(n) | Dangerous if n > 5,000-10,000 |
| Backtracking (k choices, depth d) | O(d) | Usually safe (d is small) |
| DFS on graph (n nodes) | O(n) | Dangerous for large n |
The fix: Convert deep recursion to an iterative approach using an explicit stack:
Note that TypeScript (and JavaScript) does not optimize tail recursion in practice. While the ES6 spec includes tail call optimization, only Safari implements it. Node.js and V8 do not. If your recursion depth is proportional to input size and the input can be large, convert to iteration.
Graphs appear in a large portion of DSA problems. TypeScript does not have a built-in graph class, so you need to build representations yourself. There are two common approaches:
Approach 1: Adjacency list with `Array<number[]>` -- Use when nodes are numbered 0 to n-1.
Approach 2: Adjacency list with `Map<number, number[]>` -- Use when node IDs are not contiguous or are very large.
Weighted graphs use tuples to store neighbor and weight:
| Approach | Pros | Cons | Use When |
|---|---|---|---|
Array<number[]> | Fast index access, no hashing overhead | Wastes space if node IDs are sparse | Nodes are 0 to n-1 |
Map<number, number[]> | Handles any node IDs, no wasted space | Slightly slower due to hashing | Node IDs are large, sparse, or non-numeric |
Unlike Python, TypeScript does not have a built-in tuple at runtime. But TypeScript's type system lets you type arrays as tuples, which gives you fixed-length, typed positions:
Option 1: TypeScript tuple types (preferred for typed code)
Option 2: Plain arrays (when you want flexibility)
Option 3: Interface (when clarity matters)
In practice, typed tuples [number, number] are the standard approach in TypeScript DSA. They combine type safety with conciseness.
Many DSA problems require avoiding duplicate results (e.g., 3Sum, 4Sum, permutations with duplicates). TypeScript offers two approaches:
Approach 1: Sort and skip duplicates (preferred for sorted array problems)
This is the standard approach for problems like 3Sum and 4Sum. It runs in O(1) extra space (beyond the sort) and produces results in sorted order.
Approach 2: Use a Set to collect unique results
Since Sets compare objects by reference, you must serialize (e.g., JSON.stringify) to detect duplicate arrays. This is slower than the sort-and-skip approach but simpler to implement.
These are the small patterns and utility calls you will reach for repeatedly.
Swapping elements (TypeScript's destructuring makes this clean):
Sentinel values for tracking min/max:
Infinity and -Infinity are globally available. They compare correctly with any number: Infinity > 1e18 is true.
Math utilities:
Ceiling division without floating point:
Type conversions:
`parseInt` always needs a radix. Without the second argument, parseInt("08") might be interpreted as octal in some environments. Always pass 10:
Deep copy vs shallow copy:
DSA problems often define custom node classes. TypeScript classes give you type-safe definitions with clean syntax:
TypeScript also supports a shorthand constructor syntax that reduces boilerplate:
Interfaces vs Classes: TypeScript also lets you define types with interfaces. For DSA, classes are more common because LeetCode provides them, but interfaces are useful for typing function parameters:
Building linked lists concisely with constructor chaining: