Skip to content
JL

Home

About

Blog

Contact

Shop

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Union Types: Multiple Possibilities in TypeScript

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Mastering TypeScript Utility Types
TypeScript
9m
Feb 1, 2026

Mastering TypeScript Utility Types

TypeScript provides powerful built-in utility types that help you transform existing types without rewriting them. This guide covers the most essential utility types and how to use them effectively in your projects.

#TypeScript#Utility Types+4
The any, unknown, and never Types
TypeScript
8m
Dec 8, 2025

The any, unknown, and never Types

TypeScript has three special types that handle edge cases: any (the escape hatch), unknown (the type-safe alternative), and never (for impossible values). Understanding when and how to use these types is crucial for writing safe yet flexible TypeScript code. This guide covers the differences between these types, shows you how to work with unknown safely, and demonstrates how never enables exhaustive type checking and prevents runtime errors. Learn to use these types effectively while maintaining type safety.

#TypeScript#Type System+5
Basic Types in TypeScript
TypeScript
9m
Nov 23, 2025

Basic Types in TypeScript

TypeScript's type system starts with fundamental building blocks: primitive types like string, number, and boolean, along with arrays and type annotations. Understanding these basic types is essential for writing type-safe code. This comprehensive guide walks you through each primitive type, shows you how to use type annotations effectively, and teaches you best practices for working with arrays and basic data structures in TypeScript.

#TypeScript#Types+5
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • What Are Union Types
  • The Pipe Syntax
  • Accessing Common Properties
  • Narrowing Union Types
    • Narrowing with typeof
    • Narrowing with instanceof
    • Narrowing with in
  • Real-World Patterns
  • Best Practices
  • Key Takeaways
TypeScript

Union Types: Multiple Possibilities in TypeScript

April 5, 2026•6 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript union types visualization
Union Types: Multiple Possibilities in TypeScript

What Are Union Types

A function that formats a user ID should accept both strings and numbers — sometimes IDs are numeric, sometimes they're UUIDs. In plain JavaScript you'd just accept anything and hope for the best. TypeScript gives you a better option: a union type that says "this value is either a string or a number, nothing else."

Union types model the reality that many values in software aren't one fixed thing. An API response might succeed or fail. A configuration value might be a string or an array of strings. A payment method might be a credit card, a bank transfer, or a PayPal account. Union types let you express all of these possibilities precisely, while TypeScript ensures you handle each case correctly.

Union Types Express OR Logic

A union type A | B means a value can be either type A or type B. TypeScript tracks which type is active at any given point in your code, and will only allow operations that are valid for all members of the union — or for the specific member you've narrowed to.

The Pipe Syntax

Union types are written with the | (pipe) operator between two or more types:

let id: string | number;
 
id = "abc-123"; // valid
id = 42; // valid
id = true; // Error: type 'boolean' is not assignable to type 'string | number'

You can union any types together — primitives, object types, literal types, even other unions:

type StringOrNumber = string | number;
type NullableString = string | null;
type ThreeWay = string | number | boolean;
 
// Object types in a union
type Cat = { kind: "cat"; meows: boolean };
type Dog = { kind: "dog"; barks: boolean };
type Pet = Cat | Dog;

Unions work anywhere a type annotation is valid: variable declarations, function parameters, return types, and within interfaces or type aliases.

Accessing Common Properties

When you have a union type, TypeScript only lets you access properties that exist on every member of the union. If a property only exists on some members, TypeScript blocks access until you narrow the type.

type Circle = {
  kind: "circle";
  radius: number;
};
 
type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
};
 
type Shape = Circle | Rectangle;
 
function describe(shape: Shape) {
  console.log(shape.kind); // OK — both Circle and Rectangle have 'kind'
  console.log(shape.radius); // Error: property 'radius' does not exist on type 'Rectangle'
}

kind is safe to access because it exists on both Circle and Rectangle. radius is blocked because Rectangle doesn't have it — TypeScript can't guarantee it's safe without knowing which shape you're working with.

Design Unions Around Shared Properties

Adding a shared discriminant property like kind or type to every union member makes narrowing straightforward. You'll see this pattern frequently in production codebases and it's covered in depth when we get to discriminated unions.

Narrowing Union Types

Narrowing is the process of using runtime checks to tell TypeScript which member of a union you're dealing with. Once narrowed, TypeScript unlocks access to properties and methods specific to that type.

Narrowing with typeof

The typeof operator narrows primitive unions:

function formatId(id: string | number): string {
  if (typeof id === "string") {
    // TypeScript knows id is string here
    return id.toUpperCase();
  }
  // TypeScript knows id is number here
  return id.toFixed(0);
}
 
console.log(formatId("abc-123")); // "ABC-123"
console.log(formatId(42)); // "42"

TypeScript understands typeof checks and uses them to narrow the type within each branch of the if statement.

Narrowing with instanceof

instanceof narrows class-based types:

class ApiError {
  constructor(
    public message: string,
    public statusCode: number
  ) {}
}
 
class NetworkError {
  constructor(
    public message: string,
    public isTimeout: boolean
  ) {}
}
 
type AppError = ApiError | NetworkError;
 
function handleError(error: AppError): void {
  if (error instanceof ApiError) {
    console.log(`API ${error.statusCode}: ${error.message}`);
  } else {
    // TypeScript knows error is NetworkError here
    console.log(
      `Network error (timeout: ${error.isTimeout}): ${error.message}`
    );
  }
}

Narrowing with in

The in operator checks whether a property exists on an object — useful for narrowing plain object unions where typeof won't help:

type Cat = { meows: boolean };
type Dog = { barks: boolean };
type Pet = Cat | Dog;
 
function makeNoise(pet: Pet): void {
  if ("meows" in pet) {
    // TypeScript knows pet is Cat here
    console.log(pet.meows ? "Meow!" : "...");
  } else {
    // TypeScript knows pet is Dog here
    console.log(pet.barks ? "Woof!" : "...");
  }
}

Narrowing Must Be Exhaustive

If you don't handle every member of a union, TypeScript may still report errors when accessing member-specific properties. Use else branches or explicit checks to cover all cases. The never type (covered in a later post) helps ensure you haven't missed any.

Real-World Patterns

Union types are especially useful for modeling API responses that can succeed or fail:

type SuccessResponse = {
  status: "success";
  data: User[];
  total: number;
};
 
type ErrorResponse = {
  status: "error";
  message: string;
  code: number;
};
 
type ApiResponse = SuccessResponse | ErrorResponse;
 
async function fetchUsers(): Promise<ApiResponse> {
  try {
    const response = await fetch("/api/users");
    const data = await response.json();
    return { status: "success", data: data.users, total: data.total };
  } catch (err) {
    return { status: "error", message: "Failed to fetch users", code: 500 };
  }
}
 
function renderUsers(response: ApiResponse): void {
  if (response.status === "success") {
    console.log(`Showing ${response.total} users`);
    response.data.forEach((user) => console.log(user.name));
  } else {
    console.error(`Error ${response.code}: ${response.message}`);
  }
}

Narrowing on status is enough — TypeScript knows which branch has data and which has message. This pattern of using a literal string property to discriminate a union is called a discriminated union, and it's one of the most powerful patterns in TypeScript's type system.

Best Practices

Use union types when:

  • A parameter genuinely accepts multiple types (string | number)
  • A value can be absent (string | null, User | undefined)
  • A response can have different shapes depending on outcome (SuccessResponse | ErrorResponse)

Avoid unions when:

  • You're tempted to use any — a union is almost always more precise
  • The union has too many unrelated members (consider a base type or interface instead)
// Good — a config value that can be a string or array of strings
type Origins = string | string[];
 
// Avoid — overly broad union that sidesteps type safety
type Anything = string | number | boolean | object | null | undefined;
// Use unknown instead if you truly don't know the type

When a function can return a value or null, consider whether null is truly meaningful in your domain. Often, returning an empty array or a default object is cleaner than forcing callers to handle null — but when absence is meaningful, T | null is exactly right.

Key Takeaways

  • Union types use the | syntax to express that a value can be one of several types
  • TypeScript only allows access to properties shared by all members of a union until you narrow it
  • Narrow unions with typeof (primitives), instanceof (classes), or in (object properties)
  • Adding a shared literal property like kind or status to union members makes narrowing clean and exhaustive
  • Union types are the foundation of discriminated unions — the most powerful pattern in TypeScript's advanced type system

Phase 4 Begins

You've taken your first step into TypeScript's advanced type system. Union types let you model real-world flexibility while keeping full type safety. Next up: intersection types — the AND to union's OR.