Skip to content

Step 1 of 5 · Reading · ~3 min

Learn

Collections

Records and Maps

Programs constantly ask "what value goes with this key?" and "have I seen this before?". TypeScript gives you three tools for that: plain objects, Map and Set. By the end of this lesson you can pick the right one, update a stored value safely with ??, and avoid the traps that plain objects set for keys that are not ordinary words.

Plain objects and Record

For fixed, known keys a plain object is perfect, and Record<K, V> is its type:

const ages: Record<string, number> = { alice: 30, bob: 25 };

console.log(ages.alice);   // 30
ages.carol = 40;           // add a key
console.log(ages["bob"]);  // bracket syntax for a key held in a variable

Record<string, number> reads "an object whose keys are strings and whose values are numbers". The compiler stops you storing a string value there.

Plain objects hide two traps once the keys come from data rather than your own source code:

const stock: Record<string, number> = {};
stock["10"] = 1;
stock["2"] = 1;
stock["b"] = 1;
stock["a"] = 1;
console.log(Object.keys(stock));   // ["2", "10", "b", "a"]

Keys that look like whole numbers are listed first and in numeric order, ahead of everything else, whatever order you added them in. So an object cannot remember insertion order for such keys. The second trap is inheritance: an object starts out with keys it did not get from you.

const bag: Record<string, number> = {};
console.log(typeof bag["toString"]);   // "function", not "undefined"
console.log(bag["constructor"]);       // a function, so bag["constructor"] + 1 is a string

A word like constructor or toString looks "already present" in a plain object, which corrupts counts keyed by arbitrary words.

Map: the real dictionary

const stock: Map<string, number> = new Map();

stock.set("bolts", 40);
stock.get("bolts");     // 40, or undefined if the key is absent
stock.has("nuts");      // false
stock.size;             // 1
stock.delete("bolts");  // true when something was removed

for (const [item, qty] of stock) {   // entries come out in insertion order
    console.log(item, qty);
}

A Map accepts any key type, remembers insertion order exactly, starts empty (no inherited keys), and has an honest .size. Choose it when keys come from data. Choose a plain object or Record when the keys are a fixed, known set you write out yourself.

Updating a stored value uses nullish coalescing:

stock.set(item, (stock.get(item) ?? 0) + delivered);

?? supplies the fallback only when the left side is null or undefined. || would also replace a legitimately stored 0, so ?? is the correct operator for "absent" as opposed to "falsy". Because get returns V | undefined, the compiler insists you handle the missing case.

Set: membership and uniqueness

const visitors: Set<string> = new Set();
visitors.add("ana");
visitors.add("ben");
visitors.add("ana");        // duplicate: silently ignored
console.log(visitors.size); // 2, and a Set uses .size, not .length
console.log(visitors.has("ben")); // true

A Set also remembers insertion order, and new Set(array) builds one straight from an array.

Common mistakes

  • Counting words in a plain object. Numeric-looking words come out in the wrong order, and words like constructor start with a bogus value.
  • counts.get(w) + 1. get may return undefined, which gives NaN; supply the fallback first.
  • || instead of ??. A stored count of 0 is treated as missing.
  • .length on a Map or Set. They have .size; .length is undefined.

Your exercise

Count how many times each word occurs, and report the words in the order they first appeared. Decide which container to use. The starter only splits the line into words; the hidden tests include words that a plain object mishandles.

Up nextPipelines: Map, Filter, ReduceCollections

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…