- TypeScript Tutorial
- TypeScript Home
- TypeScript Introduction
- TypeScript Setup
- TypeScript First Program
- TypeScript vs JavaScript
- TypeScript Data Types
- TypeScript Type Inference
- TypeScript Type Annotations
- TypeScript Interfaces
- TypeScript Enums
- TypeScript Type Aliases
- TypeScript Type Assertions
- TypeScript Variables
- TypeScript Functions
- TypeScript Functions
- TypeScript Optional Parameters
- TypeScript Default Parameters
- TypeScript Rest Parameters
- TypeScript Arrow Functions
- Classes and Objects
- Introduction to Classes
- Properties and Methods
- Access Modifiers
- Static Members
- Inheritance
- Abstract Classes
- Interfaces vs Classes
- Advanced Types
- TypeScript Union Types
- TypeScript Intersection Types
- TypeScript Literal Types
- TypeScript Nullable Types
- TypeScript Type Guards
- TypeScript Discriminated Unions
- TypeScript Index Signatures
- TypeScript Generics
- Introduction to Generics
- TypeScript Generic Functions
- TypeScript Generic Classes
- TypeScript Generic Constraints
- TypeScript Modules
- Introduction to Modules
- TypeScript Import and Export
- TypeScript Default Exports
- TypeScript Namespace
- Decorators
- Introduction to Decorators
- TypeScript Class Decorators
- TypeScript Method Decorators
- TypeScript Property Decorators
- TypeScript Parameter Decorators
- Configuration
- TypeScript tsconfig.json File
- TypeScript Compiler Options
- TypeScript Strict Mode
- TypeScript Watch Mode
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.
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
anytype, 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.
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'
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
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
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
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";
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);
}
}
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.
anyallows everything;string | numberonly allows those two, keeping your bugs to a minimum.