Polyglot
Languages TypeScript data structures
TypeScript § data-structures

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:

FeaturePlain objectMap
Key typesstring | number | symbolany
Iteration orderunspecified (mostly insertion in modern engines)insertion
sizemanual Object.keys(obj).lengthm.size
Performancehash table, optimised for shapehash table
SerialisationJSON.stringify worksmanual

The contemporary discipline:

  • Use Map for 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[] or readonly 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/WeakSet for object-keyed caches.
  • Use readonly for immutable parameters and properties.
  • Use spread (...) for immutable updates.
  • Use the Temporal polyfill or date-fns for substantial date manipulation.
  • Use keyof typeof obj for 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.