Data structures
TypeScript inherits JavaScript’s data structures and adds substantial type-level precision: typed arrays (number[], Array<string>), typed tuples ([string, number]), readonly variants (readonly T[], ReadonlyArray<T>), the Record<K, V> utility type for typed dictionaries, generic Map<K, V> and Set<T>, and the weak collections (WeakMap, WeakSet). Object types describe the shape of dictionary-like values; the index signature admits typed open-ended objects. The combination — JavaScript’s runtime structures plus TypeScript’s typed shapes — covers the routine data-structure surface.
Arrays
Two equivalent forms:
let a: number[] = [1, 2, 3]; // shorthand
let b: Array<number> = [1, 2, 3]; // generic form
let mixed: (string | number)[] = ["a", 1, "b", 2];
let nested: number[][] = [[1, 2], [3, 4]];
The shorthand is conventional; the generic form is conventional when the element type is itself complex.
Operations
const arr = [1, 2, 3];
arr.length; // 3
arr[0]; // 1
arr[arr.length - 1]; // 3
arr.at(-1); // 3 (since ES2022)
// Mutation:
arr.push(4); // [1, 2, 3, 4]
arr.pop(); // 4 (returned); arr is now [1, 2, 3]
arr.shift(); // 1 (returned); arr is now [2, 3]
arr.unshift(0); // [0, 2, 3]
arr.splice(1, 1); // remove element at index 1
arr.splice(1, 0, 99); // insert 99 at index 1
arr.sort(); // mutates
arr.reverse(); // mutates
// Non-mutating:
const concat = arr.concat([4, 5]);
const slice = arr.slice(1, 3);
const joined = arr.join(", ");
const includes = arr.includes(2);
const idx = arr.indexOf(2);
The conventional discipline favours non-mutating operations and explicit copies for shared data.
Iteration methods
arr.forEach(x => console.log(x));
arr.map(x => x * 2);
arr.filter(x => x > 0);
arr.reduce((a, b) => a + b, 0);
arr.find(x => x > 5);
arr.some(x => x > 5);
arr.every(x => x > 0);
arr.flat();
arr.flatMap(x => [x, x * 2]);
Treated in Loops and Functional patterns.
Readonly arrays
const arr: readonly number[] = [1, 2, 3];
const arr: ReadonlyArray<number> = [1, 2, 3];
// arr.push(4); // ERROR
// arr[0] = 0; // ERROR
// Iteration and read methods are admitted:
const doubled = arr.map(x => x * 2); // OK; produces a new array
The readonly modifier admits substantial immutability for parameters and return types:
function process(items: readonly number[]): number {
// items.push(0); // ERROR
return items.reduce((a, b) => a + b, 0);
}
The conventional discipline marks parameters as readonly when the function does not mutate them.
Tuples
Tuples admit fixed-length, heterogeneously-typed arrays:
let pair: [string, number] = ["alice", 30];
let triple: [number, number, string] = [1, 2, "three"];
const [name, age] = pair; // destructuring
// Optional elements:
let opt: [string, number?] = ["alice"];
// Rest elements (since TS 3.0):
let stringsThenNumber: [...string[], number] = ["a", "b", "c", 42];
let numberThenStrings: [number, ...string[]] = [42, "a", "b"];
// Labelled tuple elements (since TS 4.0):
let pair2: [name: string, age: number] = ["alice", 30];
// Readonly:
const pair3: readonly [number, number] = [1, 2];
Tuples admit substantial precision; conventional for “named return values” and React’s useState pattern.
function range(start: number, end: number): [number, number] {
return [start, end];
}
const [a, b] = range(0, 10);
Objects
Object types describe shapes:
let user: {
name: string;
age: number;
email?: string;
readonly id: number;
};
user = { name: "Alice", age: 30, id: 1 };
For reusable shapes, interfaces or type aliases:
interface User {
name: string;
age: number;
email?: string;
readonly id: number;
}
type Point = {
x: number;
y: number;
};
Index signatures
interface StringDict {
[key: string]: string;
}
const d: StringDict = {
foo: "bar",
baz: "qux",
};
For typed records, Record<K, V> is conventionally clearer:
type Settings = Record<string, string>;
type Counts = Record<"active" | "inactive", number>;
Iteration
const obj = { a: 1, b: 2, c: 3 };
for (const key of Object.keys(obj)) { /* ... */ }
for (const value of Object.values(obj)) { /* ... */ }
for (const [key, value] of Object.entries(obj)) { /* ... */ }
// Destructuring:
const { a, b, ...rest } = obj;
A subtle point: Object.keys, Object.values, Object.entries return arrays typed string[] (not the precise key type). The conventional defence:
const obj = { a: 1, b: 2 };
type Key = keyof typeof obj; // "a" | "b"
(Object.keys(obj) as Key[]).forEach(k => {
obj[k]; // typed precisely
});
Map<K, V>
The Map is a key-value collection with arbitrary key types:
const m = new Map<string, number>();
m.set("alice", 30);
m.set("bob", 25);
m.get("alice"); // 30
m.has("alice"); // true
m.delete("bob");
m.size; // 1
for (const [key, value] of m) { /* ... */ }
for (const key of m.keys()) { /* ... */ }
for (const value of m.values()) { /* ... */ }
m.clear();
Keys may be any type — including objects:
const userMap = new Map<User, Permissions>();
userMap.set(user1, { read: true, write: false });
The conventional choice between Map and a plain object:
| Feature | Plain object | Map |
|---|---|---|
| Key types | string | number | symbol | any |
| Iteration order | unspecified (mostly insertion in modern engines) | insertion |
size | manual Object.keys(obj).length | m.size |
| Performance | hash table, optimised for shape | hash table |
| Serialisation | JSON.stringify works | manual |
The contemporary discipline:
- Use
Mapfor genuinely dynamic key-value data. - Use plain objects for structural records and JSON-compatible data.
Set<T>
A Set is a unique-value collection:
const s = new Set<number>([1, 2, 3, 2, 1]);
console.log(s.size); // 3 (duplicates removed)
s.add(4);
s.has(2); // true
s.delete(2);
for (const x of s) { /* ... */ }
// Set operations (since ES2025):
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const inter = a.intersection(b); // Set {2, 3}
const uni = a.union(b); // Set {1, 2, 3, 4}
const diff = a.difference(b); // Set {1}
The conventional uses are deduplication and membership tests:
const unique = [...new Set(items)]; // dedupe an array
const seen = new Set<string>();
for (const item of items) {
if (seen.has(item.id)) continue;
seen.add(item.id);
process(item);
}
WeakMap<K, V> and WeakSet<T>
Weak collections admit garbage-collectable keys:
const cache = new WeakMap<object, ComputedValue>();
function compute(obj: object): ComputedValue {
let cached = cache.get(obj);
if (!cached) {
cached = expensiveCompute(obj);
cache.set(obj, cached);
}
return cached;
}
When the key is no longer referenced elsewhere, the entry is automatically removed. The conventional uses are caches and metadata associated with object identities.
The principal restrictions:
- Keys must be objects.
- The collection is not iterable.
- The size is not exposed.
Records via Record<K, V>
The Record<K, V> utility admits typed dictionaries:
type Permissions = Record<string, boolean>;
const perms: Permissions = {
read: true,
write: false,
execute: true,
};
type StatusCounts = Record<"active" | "inactive" | "banned", number>;
const counts: StatusCounts = {
active: 10,
inactive: 5,
banned: 2,
};
For closed key sets, the Record<K, V> admits substantial type safety; missing keys produce errors.
Date
The Date is JavaScript’s date/time type:
const now = new Date();
const specific = new Date("2026-01-15T10:00:00Z");
const fromMs = new Date(1736937600000);
const fromComponents = new Date(2026, 0, 15); // month is 0-indexed!
now.getFullYear();
now.getMonth(); // 0-11
now.getDate(); // 1-31
now.getHours();
now.toISOString();
now.toLocaleDateString();
now.getTime(); // milliseconds since epoch
// Arithmetic:
const future = new Date(now.getTime() + 60_000); // 1 minute later
const diff = future.getTime() - now.getTime(); // milliseconds
The conventional date library is date-fns, dayjs, or Luxon; the built-in Date admits substantial pitfalls (mutable, 0-indexed months, timezone confusion).
The Temporal API (TC39 Stage 3 as of 2026) is the conventional successor; @js-temporal/polyfill admits early use.
Regular expressions
const re = /pattern/flags;
const re2 = new RegExp("pattern", "flags");
const re3 = /^[a-z]+$/i; // case-insensitive
re3.test("HELLO"); // true
const matches = "abc 123 def 456".match(/\d+/g); // ["123", "456"]
const replaced = "hello".replace(/l/g, "L"); // "heLLo"
// Capture groups:
const m = "2026-01-15".match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (m) {
const [_, year, month, day] = m;
}
// Named capture groups:
const m2 = "2026-01-15".match(/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/);
if (m2?.groups) {
console.log(m2.groups.year); // "2026"
}
JavaScript’s regex syntax is conventional Perl-compatible; some advanced features (lookbehind, named groups, Unicode property escapes) require recent runtimes.
Common patterns
Array operations
const items = [1, 2, 3, 4, 5];
const evens = items.filter(x => x % 2 === 0);
const doubled = items.map(x => x * 2);
const sum = items.reduce((a, b) => a + b, 0);
const max = Math.max(...items);
const sorted = [...items].sort((a, b) => b - a);
const unique = [...new Set(items)];
const reversed = [...items].reverse();
Object operations
const user = { name: "Alice", age: 30, email: "a@b.c" };
const keys = Object.keys(user);
const values = Object.values(user);
const entries = Object.entries(user);
// Update:
const updated = { ...user, age: 31 };
// Pick:
const { name, age } = user;
// Omit:
const { email, ...rest } = user;
Map iteration with type safety
const counts: Record<string, number> = {};
for (const word of words) {
counts[word] = (counts[word] ?? 0) + 1;
}
// Or with Map:
const counts2 = new Map<string, number>();
for (const word of words) {
counts2.set(word, (counts2.get(word) ?? 0) + 1);
}
Group by
const grouped: Record<string, Item[]> = {};
for (const item of items) {
(grouped[item.category] ??= []).push(item);
}
// Or with Map:
const grouped2 = new Map<string, Item[]>();
for (const item of items) {
if (!grouped2.has(item.category)) grouped2.set(item.category, []);
grouped2.get(item.category)!.push(item);
}
// ES2024:
// const grouped3 = Object.groupBy(items, item => item.category);
Counting with Map
function countOccurrences<T>(items: T[]): Map<T, number> {
const counts = new Map<T, number>();
for (const item of items) {
counts.set(item, (counts.get(item) ?? 0) + 1);
}
return counts;
}
Union via Set
function union<T>(...sets: Set<T>[]): Set<T> {
const result = new Set<T>();
for (const s of sets) {
for (const x of s) result.add(x);
}
return result;
}
Memoization with WeakMap
const cache = new WeakMap<object, ComputedResult>();
function compute(obj: object): ComputedResult {
let cached = cache.get(obj);
if (!cached) {
cached = doExpensive(obj);
cache.set(obj, cached);
}
return cached;
}
Tuples for multi-return
function divmod(a: number, b: number): [number, number] {
return [Math.floor(a / b), a % b];
}
const [q, r] = divmod(17, 5);
Branded type for IDs
type UserId = string & { readonly __brand: "UserId" };
type GroupId = string & { readonly __brand: "GroupId" };
function makeUserId(s: string): UserId { return s as UserId; }
Treated in Types.
Type-safe object access
type Settings = {
host: string;
port: number;
debug: boolean;
};
function get<K extends keyof Settings>(s: Settings, key: K): Settings[K] {
return s[key];
}
const port = get(settings, "port"); // type: number
const host = get(settings, "host"); // type: string
Immutable updates
const user = { name: "Alice", age: 30, address: { city: "Helsinki" } };
// Top-level update:
const updated = { ...user, age: 31 };
// Nested update:
const updated2 = {
...user,
address: { ...user.address, city: "Stockholm" },
};
// Array updates:
const items = [1, 2, 3];
const appended = [...items, 4];
const updated3 = items.map((x, i) => (i === 1 ? 99 : x));
const removed = items.filter((_, i) => i !== 1);
For substantial immutability, libraries like Immer admit “drafts” with imperative syntax.
A note on the conventional discipline
The contemporary TypeScript data-structures advice:
- Use arrays (
T[]orreadonly T[]) for sequences. - Use tuples for fixed-length heterogeneous data.
- Use objects or
Record<K, V>for structural records. - Use
Map<K, V>for genuinely dynamic key-value data. - Use
Set<T>for unique values and membership. - Use
WeakMap/WeakSetfor object-keyed caches. - Use
readonlyfor immutable parameters and properties. - Use spread (
...) for immutable updates. - Use the
Temporalpolyfill ordate-fnsfor substantial date manipulation. - Use
keyof typeof objfor typed-key iteration. - Use branded types for nominal typing of identifiers.
The combination — arrays, tuples, objects, Map, Set, weak collections, the type-level utilities (Record, Partial, etc.) — is the substance of TypeScript’s data-structure surface. The discipline produces clear, type-safe, immutable-by-default code with substantial built-in functionality.