Stacks

Picture the stack of clean plates in a canteen. The washer-up puts each plate on top of the pile, and the next person takes the plate off the top. Nobody ever slides a plate out of the middle — you'd send the lot crashing. So the plate you take is always the one that went on most recently, and the plate at the very bottom (the first one put down) is the last one anybody will ever touch.

That single rule — last in, first out, written LIFO — is the whole idea of a stack. A stack is a collection that only lets you add and remove at one end, called the top. It sounds like a restriction, and it is; but that restriction is exactly what makes stacks so useful for "most recent first" jobs like undo, the back button, and the way a program remembers where it was.

The four operations

Because you may only touch the top, a stack has a tiny, tidy set of operations. Every stack you ever meet — in any language, in any exam board's pseudocode — offers these:

Notice what is not on the list: there is no "get the third item", no "remove from the bottom", no searching through the middle. The only item a stack will ever show you or give back is the one on top. Watch the pile grow and shrink as items are pushed and popped:

Building a Stack in code

A stack is easy to build on top of an array. We treat the end of the array as the top of the stack: push is the array's .push (add to the end), and pop is the array's .pop (remove from the end). Wrapping it in a class hides the array away and exposes only the four safe operations — you literally cannot reach into the middle, because the array is private.

class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); // add to the top (end of the array) } pop(): T | undefined { return this.items.pop(); // remove and return the top } peek(): T | undefined { return this.items[this.items.length - 1]; // look at the top, don't remove } isEmpty(): boolean { return this.items.length === 0; } size(): number { return this.items.length; } } const s = new Stack<string>(); s.push("a"); s.push("b"); console.log("Top is now:", s.peek()); // "b" — peek does not remove it console.log("Size:", s.size()); // 2 console.log("Is it empty?", s.isEmpty()); // false

The <T> just means "a stack of something" — a Stack<string> holds strings, a Stack<number> holds numbers. The logic is identical whatever T is.

Seeing LIFO with your own eyes

Here is the moment that makes "last in, first out" click. We push three plates in order — red, then green, then blue — and then pop them off one at a time. Predict the order they come back before you press Run:

class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } isEmpty(): boolean { return this.items.length === 0; } } const plates = new Stack<string>(); plates.push("red"); // in 1st plates.push("green"); // in 2nd plates.push("blue"); // in 3rd (now on top) console.log("Popping the plates off the top:"); while (!plates.isEmpty()) { console.log(" took:", plates.pop()); }

They come out blue, green, red — the exact reverse of the order they went in. That reversal is the signature of a stack: the last thing in is the first thing out. (In fact, pushing a sequence onto a stack and popping it all off again is a one-line way to reverse a list.)

Where stacks show up

The LIFO rule matches a surprising number of everyday computing jobs — always the ones where you care about the most recent thing first:

A classic exam use is checking that brackets match. Read left to right: push every opening bracket; on a closing bracket, pop the top and check it's the matching partner. If the stack is empty at the end and nothing ever mismatched, the brackets are balanced:

function bracketsMatch(text: string): boolean { const stack: string[] = []; const pairs: Record<string, string> = { ")": "(", "]": "[", "}": "{" }; for (const ch of text) { if (ch === "(" || ch === "[" || ch === "{") { stack.push(ch); // an opener — remember it } else if (ch === ")" || ch === "]" || ch === "}") { if (stack.pop() !== pairs[ch]) { // pop must match this closer return false; } } } return stack.length === 0; // nothing left unclosed? } console.log("(a[b]c) ->", bracketsMatch("(a[b]c)")); // true console.log("([)] ->", bracketsMatch("([)]")); // false, wrong order console.log("((( ->", bracketsMatch("(((")); // false, unclosed

See how naturally the stack captures "the most recently opened bracket must be the first one closed" — that is LIFO.

A stack's twin is the queue. A queue is a line at a bus stop: you join at the back and you're served from the front, so it's first in, first out (FIFO) — fair, in-order service. A stack is the opposite: last in, first out. Use a stack when the newest thing matters most (undo, back button, recursion); use a queue when things should be handled in the order they arrived (a print queue, tasks waiting their turn). Same idea of "add at one end, remove at an end" — but different ends.

The classic stack bug is stack underflow — trying to pop or peek when the stack is empty. There is no top item to hand back, so in TypeScript pop() quietly returns undefined, which then poisons whatever you do with it:

const s = new Stack<number>(); // empty! const x = s.pop(); // undefined — there was nothing to pop console.log(x + 1); // NaN — the bug spreads

Always check isEmpty() first: if (!s.isEmpty()) { const x = s.pop(); … }. In lower-level languages an underflow can crash the program outright, so this habit matters everywhere.

The second trap is forgetting the golden rule: you may only touch the top. It is tempting to think "I'll just grab the item second from the top" — but a stack offers no such operation. If you find yourself wanting to reach into the middle, a stack is the wrong data structure for the job; you probably want an array or a different structure instead.