Union Types in TypeScript

In the real world, data isn't always predictable. An API might return a user's ID as a numeric 101 or a UUID string like "abc-123". Union types are TypeScript's way of saying, "This variable could be this type OR that type." By using union types, you can write code that is flexible enough to handle multiple data formats without resorting to the dangerous any type.

Developer Tip: Think of union types as a logical "OR." If a variable is string | number, it means it can be a string OR a number at any given time.

 

Key Concepts of Union Types

  • The Pipe Symbol (|): We define a union by placing a vertical bar between types. This tells the compiler that the value can be any of the listed types.
  • Handling Uncertainty: Union types are essential when migrating JavaScript code to TypeScript or when dealing with inputs that genuinely change based on state.
  • The "Common Property" Rule: When you have a union, TypeScript only lets you access properties or methods that are available on all types in the union until you narrow it down.
  • Type Safety: Unlike the any type, union types still provide full IntelliSense and compile-time checking. If you try to assign a type that isn't in the union, TypeScript will stop you immediately.
Best Practice: Use union types instead of any whenever you know the specific set of possible types. This keeps your codebase predictable and prevents runtime crashes.

 

Example Usage of Union Types

Example 1: Basic Union Type

Imagine you are building a UI component where a width can be defined in pixels (as a number) or as a CSS string (like "50%").

let padding: string | number;

padding = 20;       // Valid: 20px
padding = "1.5rem"; // Valid: CSS units
padding = true;     // Error: Type 'boolean' is not assignable to type 'string | number'
Watch Out: If you try to call padding.toUpperCase() immediately, TypeScript will throw an error. Why? Because padding might be a number, and numbers don't have a toUpperCase method.

Example 2: Union Type with Functions

Functions often need to accept different formats of the same data. By using a "Type Guard" (like typeof), we can safely handle each case.

function printId(id: string | number) {
  if (typeof id === "string") {
    // In this block, TypeScript knows 'id' is a string
    console.log(`String ID: ${id.toUpperCase()}`);
  } else {
    // Here, TypeScript knows 'id' must be a number
    console.log(`Number ID: ${id.toFixed(2)}`);
  }
}

printId(101);       // Output: Number ID: 101.00
printId("u_99");    // Output: String ID: U_99
Common Mistake: Forgetting to "narrow" the type before using type-specific methods. You must prove to TypeScript what the type is before you use string-only or number-only functions.

Example 3: Union Type with Objects

Unions are incredibly powerful when combined with custom object types. This is common in user management systems.

type Admin = {
  role: "admin";
  permissions: string[];
};

type User = {
  role: "user";
  email: string;
};

let account: Admin | User;

account = { role: "admin", permissions: ["delete_user"] }; // Valid
account = { role: "user", email: "[email protected]" };     // Valid

 

Type Narrowing with Union Types

As we saw earlier, Type Narrowing is the process of refining a wide type (like string | number) into a more specific one. This is the "secret sauce" that makes union types safe to use.

Example: Type Guard with typeof

function formatPrice(price: string | number): string {
  if (typeof price === "number") {
    return `$${price.toLocaleString()}`;
  }
  return price; // TypeScript knows it's a string here
}

console.log(formatPrice(5000));   // Output: $5,000
console.log(formatPrice("$50"));  // Output: $50
Developer Tip: TypeScript is smart. Once you've checked if (typeof price === "number") and returned, it automatically knows that the rest of the function treats price as a string. This is called "Control Flow Analysis."

 

Union Types with Array

When you want an array to store different types of items, you must wrap the union in parentheses before the array brackets.

// An array that can contain strings OR numbers
let history: (string | number)[] = ["Created", 1, "Updated", 2];

history.push("Deleted"); // Valid
history.push(3);         // Valid
history.push({ id: 4 }); // Error: Object is not assignable to string | number
Common Mistake: Writing string | number[] instead of (string | number)[]. The first one means "either a single string OR an array of numbers." The version with parentheses means "an array that can contain both."

 

Using null and undefined with Union Types

In modern TypeScript, variables are not allowed to be null or undefined by default (if strictNullChecks is on). You must use a union to allow these values.

let username: string | null = null;

// Later in the app...
username = "CodeMaster2026"; 
Best Practice: Always use string | null for data fetched from a database or API, as fields are often empty before the data loads.

 

Union Types with Custom Types

The most advanced use of unions is the Discriminated Union. This involves adding a common property (a "discriminant") to different interfaces so you can distinguish between them easily.

interface SuccessResponse {
  status: "success"; // Discriminant
  data: string[];
}

interface ErrorResponse {
  status: "error";   // Discriminant
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === "success") {
    console.log("Results:", response.data.length);
  } else {
    console.log("Failed:", response.message);
  }
}
Developer Tip: Discriminated unions are the industry standard for managing complex state in frameworks like React or when handling Redux actions.

 

Summary

  • Flexibility: Union types let you handle data that can exist in multiple forms without losing type safety.
  • The Pipe Symbol: Use | to separate your types (e.g., string | number).
  • Narrowing is Key: Use typeof, instanceof, or property checks to tell TypeScript which specific type you are working with at runtime.
  • Better than Any: Unions provide constraints. any allows everything; string | number only allows those two, keeping your bugs to a minimum.