RizTech Academy logo
RizTech Academy
Data Structures, TypedLesson 1 of 330 min

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 need size and 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

  1. Time array.includes in a loop over two large arrays, then rewrite with a Set and time it. Compare the comparison counts you would expect.
  2. Type a Map<string, number>, set and get values, and confirm get returns number | undefined.
  3. Iterate a Map with destructuring and confirm the key and value types.
  4. Use a Set to deduplicate an array with [...new Set(arr)].
  5. Type a Record<"pending" | "paid" | "shipped", string> and confirm all keys are required.
  6. Decide, for three collections you know, whether each should be an array, Set, Map, or Record, and justify by what you do with it.
  7. Write a [string, ...string[]] non-empty type and confirm an empty array is rejected.

Official documentation

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