TypeScript Complex Data Types (and What You Can Do With Them)

Master arrays, tuples, objects, unions, intersections, generics, and utility types in TypeScript with practical patterns you can ship today. This guide shows safe operations—push, map, filter, reduce, immutable updates, and type‑narrowing—using examples from a mini CRM and an EV‑charging app. Includes a cheat sheet, common pitfalls, JSON‑LD, and copy‑paste code.


Table of contents

  1. What “complex data types” mean in TypeScript
  2. Arrays: push, map, filter, reduce (mutable vs immutable)
  3. Tuples: typed positions, labels, and variadic tuples
  4. Objects with interfaces & type aliases
  5. Unions and safe narrowing (type guards)
  6. Intersections: combining types without tears
  7. Enums vs literal unions
  8. Generics, keyof, mapped & conditional types
  9. Record, Map, Set—when to use each
  10. Utility types (cheat sheet)
  11. Worked examples (CRM, EV‑charging)
  12. Common pitfalls & best practices
  13. FAQ: “Push into an interface array field?”
  14. Article schema (JSON‑LD)
  15. Conclusion

1) What “complex data types” mean in TypeScript

Beyond primitives (string, number, boolean, bigint, symbol, null, undefined), “complex” types include:

  • Arrays (T[] / Array<T>)
  • Tuples (fixed/known positions)
  • Objects (via interface or type)
  • Unions (A | B) and Intersections (A & B)
  • Generics and advanced forms: mapped, conditional, template‑literal types
  • Collections: Map<K,V>, Set<T>
  • Utility types: Partial, Pick, Record, etc.

These features let you model real‑world data precisely and operate on it safely.


2) Arrays: push, map, filter, reduce (mutable vs immutable)

interface Item {
  id: string;
  price: number;
  tags: string[];
}

const items: Item[] = [
  { id: "a1", price: 10, tags: ["sale"] },
  { id: "b2", price: 25, tags: ["new", "popular"] },
];

// ✅ Push (mutable)
items.push({ id: "c3", price: 15, tags: [] });

// ✅ Immutable add
const moreItems = [...items, { id: "d4", price: 30, tags: ["gift"] }];

// Transform safely
const prices = items.map(i => i.price);          // number[]
const popular = items.filter(i => i.tags.includes("popular")); // Item[]
const total = items.reduce((sum, i) => sum + i.price, 0);      // number

Readonly arrays protect against accidental mutation:

const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // ❌ Error
const ro2 = [...ro, 4]; // ✅ create a new array

Tip: Prefer immutable updates in UI state (React/Angular) to avoid tricky bugs.


3) Tuples: typed positions, labels, and variadic tuples

type Point = [x: number, y: number];       // labeled tuple
const p: Point = [10, 20];

type Result = [ok: true, value: string] | [ok: false, error: Error];

function handle(r: Result) {
  if (r[0]) r[1].toUpperCase();
  else r[1].message;
}

// Variadic tuples (e.g., prepend)
type Prepend<H, T extends unknown[]> = [H, ...T];

Tuples shine when data is positional (x/y, HTTP status+payload, etc.).


4) Objects with interfaces & type aliases

interface Address {
  city: string;
  country: string;
  line2?: string;      // optional
  readonly zip: string;
}

type UserId = string & { __brand: "UserId" }; // nominal-ish branding

interface User {
  id: UserId;
  name: string;
  address: Address;
  tags: string[];
}

const user: User = {
  id: "u1" as UserId,
  name: "Ada",
  address: { city: "Bucharest", country: "RO", zip: "010001" },
  tags: ["admin"],
};

// Immutable update
const updated: User = {
  ...user,
  address: { ...user.address, city: "Cluj-Napoca" },
};

Use interface for extendable object shapes. Use type for unions, intersections, and compositions.


5) Unions and safe narrowing (type guards)

type Payment =
  | { kind: "card"; last4: string }
  | { kind: "cash" }
  | { kind: "invoice"; number: string };

function label(p: Payment) {
  switch (p.kind) {
    case "card": return `Card •••• ${p.last4}`;
    case "cash": return "Cash";
    case "invoice": return `Invoice #${p.number}`;
  }
}

// Custom type guard
function hasTags(x: unknown): x is { tags: string[] } {
  return typeof x === "object" && x !== null && Array.isArray((x as any).tags);
}

Discriminated unions + narrowing keep control flow type‑safe.


6) Intersections: combining types without tears

type Timestamped = { createdAt: Date; updatedAt: Date };
type WithOwner = { ownerId: UserId };

type OwnedEntity = Timestamped & WithOwner;

If overlapping keys conflict, TypeScript will complain—fix the definitions, don’t silence errors.


7) Enums vs literal unions

Prefer literal unions + as const for simplicity and tree‑shaking:

const Status = ["active", "inactive", "pending"] as const;
type Status = typeof Status[number]; // "active" | "inactive" | "pending"

Use enum when you need a runtime value with reverse mapping or stable numeric codes.


8) Generics, keyof, mapped & conditional types

// Generics with constraints
function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
  return keys.map(k => obj[k]);
}

type ApiResponse<T> = { ok: true; data: T } | { ok: false; error: string };

// Indexed access & `keyof`
type UserName = User["name"];    // string
type UserKeys = keyof User;       // "id" | "name" | "address" | "tags"

// Mapped types
type ReadonlyExcept<T, K extends keyof T> = {
  readonly [P in keyof T]: T[P]
} & { -readonly [P in K]: T[P] };

// Conditional types
type NonNullableProps<T> = { [K in keyof T]: NonNullable<T[K]> };

// Template literal types
type EventName<Scope extends string, Action extends string> = `${Scope}:${Action}`;

9) Record, Map, Set—when to use each

  • Record<K,V>: plain object dictionary (serializable, fast for string keys).
  • Map<K,V>: when keys aren’t strings or you need ordered iteration/size.
  • Set<T>: unique collection with fast membership checks.
type PriceBook = Record<string, number>;
const pb: PriceBook = { A100: 12.5, B200: 19.9 };

const map = new Map<string, number>([["A100", 12.5]]);
map.set("B200", 19.9);

const tags = new Set<string>(["new", "sale"]);
tags.add("popular");

10) Utility types (cheat sheet)

  • Partial<T> / Required<T>
  • Readonly<T> / custom Mutable<T> via mapped types
  • Pick<T,K> / Omit<T,K>
  • Record<K,V>
  • Exclude<T,U> / Extract<T,U> / NonNullable<T>
  • ReturnType<F> / Parameters<F> / InstanceType<C> / Awaited<T>
type DraftUser = Partial<User>;
type PublicUser = Pick<User, "id" | "name">;
type UserWithoutTags = Omit<User, "tags">;

11) Worked examples (CRM, EV‑charging)

11.1 Mini‑CRM: clients & appointments

type ISODate = string & { __brand: "ISODate" };

type ApptStatus = "scheduled" | "completed" | "cancelled";

interface Appointment {
  id: string;
  date: ISODate;
  durationMin: number;
  status: ApptStatus;
}

interface Client {
  id: string;
  name: string;
  email?: string;
  appointments: Appointment[];
}

// Create client
const c: Client = { id: "c1", name: "Maya", appointments: [] };

// Add appointment (mutable)
c.appointments.push({ id: "a1", date: "2025-08-12" as ISODate, durationMin: 60, status: "scheduled" });

// Immutable add
const c2: Client = {
  ...c,
  appointments: [
    ...c.appointments,
    { id: "a2", date: "2025-08-20" as ISODate, durationMin: 30, status: "scheduled" },
  ],
};

// Update status immutably
function completeAppointment(client: Client, apptId: string): Client {
  return {
    ...client,
    appointments: client.appointments.map(a => a.id === apptId ? { ...a, status: "completed" } : a),
  };
}

Useful operations: totals, next upcoming appointment, grouping by status.

const totalBooked = c2.appointments
  .filter(a => a.status !== "cancelled")
  .reduce((sum, a) => sum + a.durationMin, 0);

function nextAppt(client: Client): Appointment | undefined {
  return [...client.appointments]
    .filter(a => a.status === "scheduled")
    .sort((a, b) => a.date.localeCompare(b.date))[0];
}

11.2 EV‑charging: stations, sessions, and statuses

// Literal union + template literals for event names
const ChargerStatus = ["available", "occupied", "out_of_service"] as const;
type ChargerStatus = typeof ChargerStatus[number];

type StationId = string & { __brand: "StationId" };

type EventName = `station:${string}` | `session:${"start" | "stop"}`;

interface Station {
  id: StationId;
  name: string;
  status: ChargerStatus;
  connectors: ReadonlyArray<{
    id: string;
    maxKw: number;
  }>;
}

interface Session {
  id: string;
  stationId: StationId;
  kWh: number;
  cost: number;
  startedAt: ISODate;
  stoppedAt?: ISODate;
}

// Dictionary of station status
const byStation: Record<StationId, ChargerStatus> = {} as Record<StationId, ChargerStatus>;

function setStatus(id: StationId, status: ChargerStatus) {
  byStation[id] = status; // plain object Record
}

// Using a Map when keys are not always strings or we want iteration order
const sessions = new Map<string, Session>();
sessions.set("s1", { id: "s1", stationId: "st-001" as StationId, kWh: 10.2, cost: 23.4, startedAt: "2025-08-10" as ISODate });

Aggregations & projections

// Sum energy per station
function kWhByStation(list: Session[]): Record<StationId, number> {
  return list.reduce((acc, s) => {
    const key = s.stationId as StationId;
    acc[key] = (acc[key] ?? 0) + s.kWh;
    return acc;
  }, {} as Record<StationId, number>);
}

// Generic pluck of session fields
function pluckSessions<K extends keyof Session>(list: Session[], key: K): Session[K][] {
  return list.map(s => s[key]);
}

12) Common pitfalls & best practices

  • Turn on strict (or at least strictNullChecks, noImplicitAny).
  • Prefer immutable updates for UI state (spread over push/splice).
  • Use discriminated unions and narrow early.
  • Prefer literal unions over enums unless you need runtime features.
  • Avoid double assertions (x as unknown as T). Model types so you don’t need them.
  • Consider branding identifiers (type StationId = string & { __brand: "StationId" }) to prevent mix‑ups.
  • Keep collections readonly where possible; copy to mutate.

13) FAQ: “Push into an interface array field?”

Yes—treat it like any typed array:

interface Bucket {
  name: string;
  values: number[];
}

const b: Bucket = { name: "scores", values: [10, 20] };
b.values.push(30);                         // ✅ mutable
const b2: Bucket = { ...b, values: [...b.values, 40] }; // ✅ immutable

If the array is readonly, create a new array instead of push.


14) Article schema (JSON‑LD)

Paste into your head (or via an SEO plugin):

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  "headline": "TypeScript Complex Data Types (and What You Can Do With Them)",
  "description": "Learn TypeScript complex data types—arrays, tuples, unions, generics—and safe operations with CRM & EV‑charging examples.",
  "author": { "@type": "Person", "name": "Matrix Rom" },
  "datePublished": "2025-08-11",
  "inLanguage": "en"
}
</script>

15) Featured image prompt (1200×630 suggested)

Prompt: Flat, modern illustration of interconnected TypeScript shapes (array brackets, tuple nodes, union/intersection venn, T for generics) forming a clean data‑flow diagram. Subtle code snippets in the background. Corporate‑tech aesthetic, high contrast on a dark‑to‑indigo gradient, crisp edges, isometric accents, no people. Include a small TypeScript “TS” badge in a corner.

Alt text: “Abstract diagram of TypeScript complex data types—arrays, tuples, unions, generics—connected in a flow.”


16) Conclusion

Mastering arrays, tuples, unions/intersections, and generics unlocks TypeScript’s real power: safe transformations and predictable data flows. Start with clean interfaces, narrow aggressively, and lean on utility/mapped/conditional types to keep your code expressive and DRY.

This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back