Arrays, tuples, Map, Set and Record — and what each costs
You hold data in collections all day — a list of users, a lookup of prices, a set of seen ids. TypeScript
types all of JavaScript's collections precisely, and choosing the right one — with an eye on what each
costs — is a real engineering skill, not just a typing exercise. This lesson is arrays, tuples, Map,
Set, and Record, and the performance trap that decides between them.
Arrays and tuples — recap with depth
You met these in the basic-types module. The essentials, with the costs that matter:
const prices: number[] = [120, 340, 90]; // any number of numbers
const coord: [number, number] = [18.52, 73.85]; // exactly two, fixed positions
Arrays are ordered and allow duplicates. Their costs, which you should know: indexing (arr[i]) and
appending (push) are instant, but includes/indexOf scan the whole array — so checking "is
this in the array?" is slow on a large one. Iterating and map/filter are linear (touch every
element). This is fine for lists you process, but if you repeatedly ask "is X present?", an array is the
wrong tool — a Set is right, as the performance section below shows.
TypeScript also has variadic tuples for advanced cases — a tuple with a fixed start and a rest:
type NonEmpty = [string, ...string[]]; // at least one string, then any number more
const a: NonEmpty = ["Pune"]; // fine
const b: NonEmpty = []; // error — needs at least one
[string, ...string[]] types a "non-empty array" — the first element required, the rest optional. This
is a genuinely useful shape (a function that needs at least one argument), and it shows tuples doing more
than a fixed pair.
Map<K, V> — a typed dictionary
A Map associates keys with values, and — unlike a plain object — its keys can be any type, not just
strings:
const prices = new Map<string, number>(); // string keys, number values
prices.set("chai", 15);
prices.set("samosa", 20);
prices.get("chai"); // number | undefined — a missing key gives undefined
prices.has("chai"); // boolean
prices.delete("chai"); // boolean
prices.size; // number
for (const [item, price] of prices) { /* item: string, price: number */ }
Map<string, number> (generic in both key and value types) is checked on both — you cannot set a
wrong-typed key or value. Note get returns V | undefined (a missing key is undefined) — TypeScript
forces you to handle the miss, the null-safety theme again. A Map beats a plain object when: the keys
are dynamic or not strings, you need the size, you iterate frequently (a Map preserves insertion
order and iterates cleanly), or you add/remove keys often. Reach for a Map for a real dynamic
collection; use a plain object (or Record) for a fixed, known set of keys.
Set<T> — unique values, fast membership
A Set holds unique values and is built for one question: "is this in here?"
const seen = new Set<string>();
seen.add("Pune");
seen.add("Mumbai");
seen.add("Pune"); // ignored — already present
seen.has("Pune"); // true — this is FAST regardless of size
seen.size; // 2 — the duplicate was not counted
seen.delete("Mumbai"); // boolean
const unique = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3] — dedupe an array in one line
Set<string> holds unique strings; adding a duplicate does nothing. The key property: has is
effectively instant even on a huge set (a hash lookup), where an array's includes scans. So a Set
is the right tool for "have I seen this id?", "is this tag selected?", or deduplicating — the last of
which is the neat [...new Set(array)] idiom.
Record<K, V> versus Map — object or collection?
Record<K, V> (the utility type) types a plain object used as a dictionary:
type Prices = Record<string, number>;
const menu: Prices = { chai: 15, samosa: 20 }; // a plain object, typed
type StatusColor = Record<"pending" | "paid" | "shipped", string>; // exactly these keys required
The choice between Record (a plain object) and Map:
Record(plain object) — when the keys are known and fixed (a config, a lookup table for a literal union), keys are strings/numbers/symbols, and you want plain-object ergonomics and JSON serialisation.Record<"pending" | "paid", string>even requires every key — great for exhaustive lookups.Map— when the keys are dynamic (you do not know them in advance), the keys are non-string types, you add/remove often, or you needsizeand clean iteration.
The rule of thumb: fixed known keys → Record/object; dynamic collection → Map. A common beginner
mistake is using a plain object as a dynamic dictionary and hitting its quirks (prototype keys, no
size, string-only keys) where a Map would have been cleaner.
The performance trap: array includes in a loop
The data-structure choice that catches people, exactly as in other languages: checking membership in an array, repeatedly, inside a loop.
const blocked = ["spam@x.com", "bad@y.com", /* ...thousands... */];
const users = [/* ...thousands... */];
// SLOW — O(users × blocked): for each user, scan the whole blocked array
const allowed = users.filter(u => !blocked.includes(u.email));
// FAST — build a Set once, then membership is instant
const blockedSet = new Set(blocked);
const allowed2 = users.filter(u => !blockedSet.has(u.email));
blocked.includes(u.email) scans the entire blocked array for every user — a nested loop whose cost
is the product of the two sizes. With thousands of each, that is millions of comparisons. Converting
blocked to a Set makes each check instant, dropping the cost from "users × blocked" to "users". Same
result, vastly less work. When you repeatedly check membership in a collection, that collection should
be a Set (or the keys of a Map), not an array. This is the single most common collection
performance mistake, and TypeScript will not warn you about it — the types are fine; only the choice is
wrong. Recognising it is what separates code that scales from code that crawls.
Check your work
Array costs. Indexing and appending are instant; includes/indexOf scan (slow on large arrays);
iteration and map/filter are linear.
What a variadic tuple like [string, ...string[]] types. A non-empty array — a fixed first element,
then any number more.
Map<K, V>. A typed dictionary with any key type; get returns V | undefined; has size and
clean ordered iteration.
Set<T>. Unique values with instant has (membership) regardless of size; [...new Set(arr)]
dedupes.
Record versus Map. Record/object for fixed known keys (and JSON); Map for dynamic
collections, non-string keys, frequent add/remove, or size.
The performance trap. Repeated array.includes inside a loop is a nested scan (product of sizes);
use a Set so each check is instant.
Why TypeScript will not catch the trap. The types are correct; only the data-structure choice is wrong — you must recognise it.
Practice
- Time
array.includesin a loop over two large arrays, then rewrite with aSetand time it. Compare the comparison counts you would expect. - Type a
Map<string, number>, set and get values, and confirmgetreturnsnumber | undefined. - Iterate a
Mapwith destructuring and confirm the key and value types. - Use a
Setto deduplicate an array with[...new Set(arr)]. - Type a
Record<"pending" | "paid" | "shipped", string>and confirm all keys are required. - Decide, for three collections you know, whether each should be an array,
Set,Map, orRecord, and justify by what you do with it. - Write a
[string, ...string[]]non-empty type and confirm an empty array is rejected.
Official documentation
- TypeScript — Everyday Types: arrays and tuples — Arrays and tuples.
- MDN — Map and Set — The collections, and their costs.
- TypeScript — Record — Typed object dictionaries.
Next: immutability — readonly, as const, and keeping data unchanged.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship