Part one of this series covered scope, closures, promises, the event loop, and TypeScript's type system. This part covers the four data structure and algorithm concepts that come up constantly in interviews, and constantly in real code, if you know where to look for them.
Same approach as before: a plain definition first, for anyone just here to check their understanding, then a real example, not a toy one.
Array, Set, and Map
An array is an ordered collection you access by position, or index. A Set stores a collection of values with no duplicates allowed, and its main job is answering "have I seen this before" efficiently. A Map stores key-value pairs, and its main job is answering "given this key, what's the value" efficiently.
The three get confused because they can all technically "hold a list of things," but they answer completely different questions.
Array, when order and position matter:
const recentSubmissions = [submission1, submission2, submission3];
recentSubmissions[0]; // the first one
Use this when you genuinely care about sequence, or need to iterate in a specific order.
Set, when you only care about membership:
const selectedLeadIds = new Set<string>();
selectedLeadIds.add("lead_123");
selectedLeadIds.add("lead_456");
selectedLeadIds.has("lead_123"); // true
This is the shape of a bulk selection feature, checking a batch of rows in a table to apply a status update to all of them at once. The question you're asking is always some version of "is this one selected," and a Set answers that in constant time instead of scanning an array on every check.
Map, when you need a fast lookup by key:
const leadsById = new Map<string, Lead>();
leadsById.set(lead.id, lead);
leadsById.get("lead_123"); // the full lead object
Anywhere you'd otherwise write array.find(item => item.id === someId) inside a loop, a Map built once up front turns repeated linear scans into repeated constant time lookups.
Finding duplicates with a Set
Say your backend receives a list of email addresses during a bulk import, a CSV of contacts someone's uploading into their account:
const emails = [
"allen@example.com",
"john@example.com",
"allen@example.com",
];
You need to flag which ones are duplicated before importing. A Set gives you a clean way to track what's already been seen while walking the list exactly once:
const seen = new Set<string>();
const duplicates = new Set<string>();
for (const email of emails) {
if (seen.has(email)) {
duplicates.add(email);
} else {
seen.add(email);
}
}
console.log([...duplicates]); // ["allen@example.com"]
This is a different shape of problem from the classic two sum pattern I covered in an earlier post on production JavaScript patterns, even though both lean on a hash-based structure. Two sum is about finding a relationship between two different values that together satisfy some condition. This is simpler: just tracking whether a single value has shown up before. Same underlying idea, a hash-based structure turning repeated scans into constant time checks, applied to a narrower question. The same pattern covers duplicate user IDs, duplicate product SKUs, and duplicate database records just as directly.
Big O
Big O describes how the amount of work a piece of code does grows as its input grows. It's not a measurement of actual speed; it's a description of the growth curve, which matters more than the raw number once your data gets large enough.
Here's the version that actually shows up in production, not the whiteboard version. Suppose you have a list of users and a separate list of profiles, and for every user you need to find their matching profile:
for (const user of users) {
const profile = profiles.find(profile => profile.userId === user.id);
}
At 100 users and 100 profiles, this does roughly 10,000 comparisons in the worst case, .find() scanning the whole profiles array for every single user. That's O(n²), quadratic, because the work grows with the product of both list sizes. At 100,000 users and 100,000 profiles, that's roughly ten billion comparisons, the kind of thing that turns a fast endpoint into a timeout with no code change other than more data showing up.
The fix is the same Map pattern from the section above, built once, ahead of time:
const profileMap = new Map(profiles.map(profile => [profile.userId, profile]));
for (const user of users) {
const profile = profileMap.get(user.id);
}
Building the map is O(n). Every lookup afterward is constant time. Total cost: O(n), not O(n²). Same result, a completely different growth curve once real data volume shows up. This is the actual reason Big O gets asked about in interviews, not to test whether you can recite notation, but to see whether you notice a nested nested nested loop before it ships and becomes a production incident.
The handful worth actually knowing by shape, not by memorized name: constant time regardless of input size, logarithmic where doubling the input barely adds work, linear where work grows directly with input, linearithmic which is what most efficient sorting algorithms cost, and quadratic, which is the one to watch for, since it's the one that quietly turns fine into broken as data grows.
Binary search
Binary search finds a value in a sorted collection by repeatedly cutting the remaining search space in half, instead of checking every element in order.
Picture a sorted list of customer IDs:
100, 200, 300, 400, 500, 600, 700
Looking for 600, you don't start at the front and check each one in sequence. You check the middle, 400. 600 is bigger, so everything to the left of 400 is eliminated in one comparison; no need to ever look at it. What's left is 500, 600, 700. Check the middle again: 600. Found it in two comparisons instead of six.
That halving is exactly why binary search runs in logarithmic time: every comparison eliminates half of what's left, so the number of comparisons needed grows far slower than the size of the data. A sorted list of a million records finds any value in roughly twenty comparisons. Double the data to two million, and it only costs one more comparison, not double the work.
The condition worth remembering, since it's the part beginners forget, and the part an interviewer is often specifically checking for: binary search only works on sorted data. Run it against an unsorted list, and it'll return wrong answers confidently, not an error, which is a worse failure mode than crashing outright.
Where this connects
Array, Set, and Map aren't three interchangeable ways to store a list; they're three different questions: what order is this in, have I seen this before, and what value belongs to this key. Big O is the language for describing what happens to your chosen structure once real data volume shows up, and binary search is a concrete example of an algorithm that only gets its efficiency because it assumes something specific about its input, sortedness, that most naive implementations don't bother to require or exploit.
The next post in this series moves from data structures into testing and debugging: how to actually test a new feature properly, what makes a good regression test, and how to diagnose a production API that suddenly got slow without guessing.
If you've hit one of these patterns in a real codebase and would explain it differently, I'd like to hear it. Reach me at allen@formgrid.dev.
I'm Allen, a full stack TypeScript engineer and the founder of Formgrid and SheetRocket. I write about real production engineering from products that people actually pay for. More at jonesstack.com.
Top comments (0)