Learn / Programming / TypeScript / Types, Interfaces and Unions

Beginner 16 min

Types, Interfaces and Unions

Model data with interfaces, unions, literals and narrowing.

What you will learn

  • Define interfaces and type aliases
  • Use union types and narrowing
  • Prefer literals over strings

Real data has structure. TypeScript lets you name that structure and reuse it, so every function that touches a User agrees on what a user looks like.

interface User {
  id: number;
  name: string;
  email?: string;          // optional
  readonly createdAt: Date; // cannot be reassigned
}

const ada: User = { id: 1, name: "Ada", createdAt: new Date() };

function greet(user: User): string {
  return `Hi ${user.name}`;
}

type Point = { x: number; y: number };

interface and type overlap heavily. A good rule: use interface for object shapes you may extend, and type for unions and everything else. Pick one convention per project.

interface Admin extends User {
  permissions: string[];
}

Union types

A union says a value can be one of several types. Combined with literal types, it models a fixed set of options far better than plain strings.

type Id = number | string;
type Status = "idle" | "loading" | "error" | "success";

function setStatus(s: Status) { /* ... */ }
setStatus("loading");      // OK
// setStatus("loding");    // Error: typo caught at compile time

Narrowing

Inside a branch that checks the type, TypeScript narrows the union automatically.

function format(id: number | string): string {
  if (typeof id === "number") {
    return id.toFixed(0);        // here id is number
  }
  return id.toUpperCase();       // here id is string
}

Discriminated unions

Give each variant a shared literal field, and TypeScript can tell them apart. This is perfect for API results and UI state.

type Result =
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };

function show(r: Result) {
  switch (r.status) {
    case "success": return r.data.join(", ");
    case "error":   return `Failed: ${r.message}`;
  }
}

If you later add a third variant and forget to handle it, the compiler flags the incomplete switch. That safety net is a major reason teams adopt TypeScript.

Enums versus literal unions

Most modern code prefers string literal unions (as above) or as const objects to enum, since they produce no extra runtime code.

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number];    // "admin" | "editor" | "viewer"

Try it yourself

Model a Shape as a discriminated union of circle (radius) and rectangle (width, height), and write area(shape) using a switch.

Show solution
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "rect":   return s.width * s.height;
  }
}