Chapter 3.1☕ 15 min read

3.1 — Union Types

When a value could be this OR that — union types have you covered.

01The Station Board

Sometimes a value isn't just one type — it could be several possible types. A phone number could be a string OR a number. A response could be a success object OR an error string. A transport choice in Hyderabad could be an auto OR a bus OR the metro.

Union types let you say: "this value is THIS type OR THAT type." That's the whole idea — you're declaring a list of acceptable types, and the value must be exactly one of them at any given time.

Think of it like standing at Secunderabad station. You need to get to HITEC City. You have options — Metro, MMTS train, or the 10K bus. You can take ANY ONE of them, but not two at the same time. You pick ONE from the union of available transport types. That's exactly what a union type is — "pick any one from this list of types."

The syntax uses the pipe | operator: string | number means "string OR number." You're telling TypeScript: "This value could be any of these types — be prepared for all of them."

// At Secunderabad station,
// you pick ONE transport
type Transport =
  | "metro"
  | "mmmts"
  | "bus"
  | "walk";

let commute: Transport;
commute = "metro"; // ✅ valid
commute = "walk";  // ✅ valid

// Union: pick any ONE type
type ID = string | number;
let userId: ID;
userId = "ABC123"; // ✅ string
userId = 42;       // ✅ number
02Union Syntax & Basics

The pipe | operator creates a union between two or more types. It's the most straightforward way to say "this or that or that other thing." Let's see how it works in practice.

Basic example: let id: string | number; — now id can hold either a string or a number. Assigning "ABC123" works. Assigning 42 works. But assigning true? ERROR! Because boolean is not part of the union.

Unions aren't limited to two types. You can chain as many as needed: type Transport = "metro" | "bus" | "auto" | "walk". This is especially powerful when combined with string literals — TypeScript will only accept exactly those values.

Unions also work with object types: type Response = SuccessObj | ErrorObj. This means a response is either a success object OR an error object — never both, never neither. This pattern is fundamental for modeling real-world data like API responses, form states, and async operations.

TypeScript forces you to handle each possibility safely. You can't just call .toUpperCase() on string | number because .toUpperCase() doesn't exist on number. You must narrow the type first (we'll cover this in depth in Stage 6). For now, know that TypeScript only allows operations that work for ALL members of the union.

// Basic union
let id: string | number;
id = "ABC123"; // ✅
id = 42;       // ✅
id = true;     // ❌ boolean!

// Union with many types
type Status =
  | "loading"
  | "success"
  | "error";

// Union with object types
type SuccessObj = {
  data: string;
};
type ErrorObj = {
  error: string;
};
type Result =
  | SuccessObj
  | ErrorObj;

// Function param with union
function formatId(
  id: string | number
): string {
  return String(id);
}

// Return type union
function parse(
  input: string
): string | null {
  if (!input) return null;
  return input.trim();
}
03Working with Unions

When you have a union type, TypeScript only lets you use properties and methods that exist on ALL members of the union. This is a crucial rule — it's TypeScript's way of keeping you safe. If you could call any method from any member, you'd crash at runtime when the value turns out to be a different type.

Example: in a function function process(value: string | number), you can safely call .toString() because both string and number have that method. But you can't call .toUpperCase() (only exists on string) or .toFixed() (only exists on number) without narrowing first.

Narrowing is how you tell TypeScript: "I know this value is specifically THIS type right now." The simplest way is using typeof: inside if (typeof value === "string"), TypeScript knows value is a string, so you can call all string methods. Similarly, if (typeof value === "number") narrows to number.

You can also narrow with equality checks: if (value === null) tells TypeScript that inside the block, value is null. This is essential for handling nullable types like string | null, which you'll see everywhere in real codebases.

Unions with arrays need special attention. const items: (string | number)[] means an array where each element is either a string or a number. The parentheses are critical here! Without them, string | number[] means something entirely different — "a single string OR an array of numbers." Always use parentheses when mixing unions with array syntax, parentheses matter!

function process(
  value: string | number
) {
  // ✅ Both have .toString()
  value.toString();

  // ❌ Only string has this
  value.toUpperCase(); // ERROR

  // ✅ Narrow with typeof
  if (typeof value === "string") {
    value.toUpperCase(); // OK!
  }

  // ✅ Narrow with typeof
  if (typeof value === "number") {
    value.toFixed(2); // OK!
  }
}

// Union with arrays
type Mixed = (string | number)[];
const items: Mixed =
  ["hello", 42, "world"];

// ⚠️ These are DIFFERENT!
// (string | number)[] =>
//   array of string or number
// string | number[] =>
//   string OR array of numbers
// string[] | number[] =>
//   string array OR number array
04Union Traps

Union types come with their own set of traps that trip up even experienced developers. Let's walk through the most common ones so you don't fall into them.

Trap 1: Parenthesis confusion with array unions. This is the number one mistake. (string | number)[] is an array of strings-or-numbers — each element can be either type. string | number[] is a single string OR an array of numbers — the value as a whole is one or the other. string[] | number[] is an array of strings OR an array of numbers — the array contains only one type, not mixed. These are three completely different types. Always use parentheses when you want a mixed array!

Trap 2: Object union property access. When you have a union of object types, you can only access properties that exist on all members of the union. Given type A = { name: string } and type B = { age: number }, trying to access x.name on x: A | B gives an ERROR because .name doesn't exist on type B. You must narrow first using "name" in x or a similar check.

Trap 3: Union vs intersection confusion. string | number means "string OR number" — the value is one of them. string & number means "string AND number" — the value must be BOTH at the same time, which is impossible, so it resolves to never. Don't mix up | and &! They look similar but do opposite things.

Trap 4: Forgetting union members. If you forget to handle a member of the union, TypeScript won't always warn you unless you use exhaustiveness checking. We'll cover this in detail in chapter 3.5 and Stage 6, but keep it in mind — always handle every case!

// ❌ Three DIFFERENT types!
type T1 = (string | number)[];
// array of strings-or-numbers

type T2 = string | number[];
// single string OR number[]

type T3 = string[] | number[];
// string[] OR number[] (not mixed!)

// ❌ Property access trap
type A = { name: string };
type B = { age: number };
function f(x: A | B) {
  x.name; // ERROR!
  // .name doesn't exist on B
}

// ✅ Fix: narrow first
function g(x: A | B) {
  if ("name" in x) {
    x.name; // OK!
  }
}

// ❌ Union vs Intersection
type U = string | number;
// string OR number ✅

type I = string & number;
// string AND number = never ❌
05Union Types Cheatsheet

Let's consolidate everything we've learned about union types into a quick-reference cheatsheet. Bookmark this section — you'll come back to it often!

  • Basic union: type T = A | B — value is type A OR type B.
  • Multiple union: type T = A | B | C | D — chain as many types as you need.
  • Union with arrays: type T = (A | B)[] — an array where each element is either A or B. Parentheses required!
  • Union with objects: type Result = Success | Error — value is one object shape or another.
  • Narrowing with typeof: if (typeof x === "string") { /* x is string */ } — TypeScript narrows the type inside the block.
  • Narrowing with in operator: if ("name" in obj) { /* obj has name */ } — narrow object unions by checking for property existence.

Key rules to remember:

  1. The pipe | means OR — at any moment, the value is exactly ONE of the listed types, not a mixture.
  2. Parentheses matter: (A | B)[]A | B[]A[] | B[]. These are three completely different types.
  3. You must narrow before using type-specific methods. TypeScript only allows operations that work on ALL union members.
  4. A & B is intersection, not union — don't confuse | with &.
  5. A union of overlapping types keeps only what's common between them.

The golden rule: "Union is the Secunderabad station board — pick ONE track, not all. And always narrow before you board!"

// 📋 Union Types Cheatsheet

// Basic
type ID = string | number;

// Multiple
type Status =
  | "loading"
  | "success"
  | "error";

// With arrays (use parens!)
type Mixed = (string | number)[];

// With objects
type Result =
  | { data: string }
  | { error: string };

// Narrowing
function handle(
  val: string | number
) {
  // Shared methods only
  val.toString(); // ✅

  // Type-specific: narrow first
  if (typeof val === "string") {
    val.toUpperCase(); // ✅
  }
}

Key Points

  • Union types use the | pipe operator to combine multiple types
  • A value of a union type is exactly ONE of the listed types at any moment
  • Only methods shared by ALL union members are available without narrowing
  • Use typeof or the in operator to narrow union types
  • Parentheses matter: (A | B)[] ≠ A | B[] ≠ A[] | B[]
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase