TypeScript generic binary search tree
Order any type with a comparator function and collect the values in sorted order.
On this page
TypeScript generics, classes, and recursion.
- Order any type with a comparator function and collect the values in sorted order.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A generic binary search tree needs a way to order its elements. Instead of requiring the element type to be comparable, the tree takes a comparator function (a, b) => number that returns a negative number, zero, or a positive number. This is the same convention as Array.prototype.sort and lets one tree class order numbers, strings, or records by any field.
Insert and search follow one path from the root, so they take O(h) time for height h. An in-order traversal visits left, node, right and yields the sorted order. The nodes are plain objects with optional left and right children, and null checks are enforced by the compiler when strict null checking is on.
Code example
type Compare<T> = (a: T, b: T) => number;
interface TreeNode<T> {
value: T;
left?: TreeNode<T>;
right?: TreeNode<T>;
}
class SortedTree<T> {
private root?: TreeNode<T>;
private compare: Compare<T>;
constructor(compare: Compare<T>) {
this.compare = compare;
}
insert(value: T): void {
const node: TreeNode<T> = { value };
if (!this.root) {
this.root = node;
return;
}
let current = this.root;
for (;;) {
const order = this.compare(value, current.value);
if (order === 0) {
return;
}
const side = order < 0 ? "left" : "right";
const child = current[side];
if (!child) {
current[side] = node;
return;
}
current = child;
}
}
has(value: T): boolean {
let current = this.root;
while (current) {
const order = this.compare(value, current.value);
if (order === 0) {
return true;
}
current = order < 0 ? current.left : current.right;
}
return false;
}
toArray(): T[] {
const out: T[] = [];
const walk = (node?: TreeNode<T>): void => {
if (!node) return;
walk(node.left);
out.push(node.value);
walk(node.right);
};
walk(this.root);
return out;
}
}
const numbers = new SortedTree<number>((a, b) => a - b);
[50, 30, 70, 20, 40, 60, 80, 40].forEach((n) => numbers.insert(n));
console.log(numbers.toArray().join(" "));
console.log(numbers.has(60), numbers.has(65));
const byLength = new SortedTree<string>((a, b) => a.length - b.length || a.localeCompare(b));
["pear", "fig", "apple", "kiwi", "fig"].forEach((w) => byLength.insert(w));
console.log(byLength.toArray().join(" "));20 30 40 50 60 70 80 true false fig kiwi pear apple
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
type Compare<T> = (a: T, b: T) => number; interface TreeNode<T> { value: T; left?: TreeNode<T>; right?: TreeNode<T>;} class SortedTree<T> { private root?: TreeNode<T>; private compare: Compare<T>; constructor(compare: Compare<T>) { this.compare = compare; } insert(value: T): void { const node: TreeNode<T> = { value }; if (!this.root) { this.root = node; return; } let current = this.root; for (;;) { const order = this.compare(value, current.value); if (order === 0) { return; } const side = order < 0 ? "left" : "right"; const child = current[side]; if (!child) { current[side] = node; return; } current = child; } } has(value: T): boolean { let current = this.root; while (current) { const order = this.compare(value, current.value); if (order === 0) { return true; } current = order < 0 ? current.left : current.right; } return false; } toArray(): T[] { const out: T[] = []; const walk = (node?: TreeNode<T>): void => { if (!node) return; walk(node.left); out.push(node.value); walk(node.right); }; walk(this.root); return out; }} const numbers = new SortedTree<number>((a, b) => a - b);[50, 30, 70, 20, 40, 60, 80, 40].forEach((n) => numbers.insert(n));console.log(numbers.toArray().join(" "));console.log(numbers.has(60), numbers.has(65)); const byLength = new SortedTree<string>((a, b) => a.length - b.length || a.localeCompare(b));["pear", "fig", "apple", "kiwi", "fig"].forEach((w) => byLength.insert(w));console.log(byLength.toArray().join(" "));Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| The tree gets its ordering from a function, so it works with any type. | — | — | |
| Negative means go left, positive means go right, zero means the value is already there. | — | — | |
| Attach the new node at the first empty child on the path. | — | — | |
| In-order traversal gives sorted numbers. The duplicate 40 was ignored. | — | 20 30 40 50 60 70 80 | |
| Search follows one path. | — | 20 30 40 50 60 70 80 true false | |
| Strings ordered by length, then alphabetically. The second fig is a duplicate and is ignored. | — | 20 30 40 50 60 70 80 true false fig kiwi pear apple |
Behind the scenes
How TypeScript processes this code
TypeScript checks types and emits JavaScript. Type annotations are removed before execution.
Cost and common mistakes
insert and has are O(h); toArray is O(n).
- A comparator must be consistent. If compare(a, b) and compare(b, a) disagree, the tree can lose values.
- A plain BST degrades to O(n) on sorted input. Use a self-balancing tree for guaranteed O(log n).
- Two values that compare as equal are treated as duplicates here, even if they are different objects.
Try it yourself
How would you sort people by age with this tree?
Reveal answer and reasoning
Create new SortedTree<Person>((a, b) => a.age - b.age). People with equal ages would count as duplicates, so break ties with another field if needed.