TypeScript Union Types
A union type allows a variable to hold a value of more than one type. Instead of restricting a variable to a single type, a union says "this value can be type A or type B." The pipe symbol | separates each type in the union. Union types handle real-world data that naturally comes in multiple forms — an ID that can be a number or a string, for example.
What Is a Union?
Single type: Union type:
+----------+ +----------+
| number | | number |
| | OR | OR |
+----------+ | string |
+----------+
let age: number let id: number | string
age = 25; ✓ id = 1001; ✓
age = "25"; ✗ id = "EMP-101"; ✓
id = true; ✗
Declaring a Union Type
// Variable that accepts both number and string let employeeId: number | string; employeeId = 1001; // Valid — number employeeId = "EMP-1001"; // Valid — string employeeId = true; // Error: boolean not in union // Union with three types let config: string | number | boolean; config = "dark"; // Valid config = 42; // Valid config = false; // Valid
Union in Function Parameters
function formatId(id: number | string): string {
return "ID: " + id.toString();
}
console.log(formatId(1001)); // ID: 1001
console.log(formatId("EMP-1001")); // ID: EMP-1001
Narrowing — Working Safely with Unions
When a variable has a union type, TypeScript does not know which specific type it holds at any moment. Before using type-specific operations, the code must check the actual type. This process is called type narrowing.
Union: number | string
|
+-----------+
| |
number string
|
Before narrowing — only methods common to both types available
After narrowing — all methods of the narrowed type available
function processInput(input: number | string): string {
if (typeof input === "string") {
// Narrowed to string — string methods available
return input.toUpperCase();
}
// Narrowed to number — number methods available
return input.toFixed(2);
}
console.log(processInput("hello")); // HELLO
console.log(processInput(3.14159)); // 3.14
Narrowing with instanceof
class Dog {
bark(): void { console.log("Woof!"); }
}
class Cat {
meow(): void { console.log("Meow!"); }
}
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark(); // Narrowed to Dog
} else {
animal.meow(); // Narrowed to Cat
}
}
makeSound(new Dog()); // Woof!
makeSound(new Cat()); // Meow!
Union Types with Arrays
// Array of mixed types let mixedList: (number | string)[] = [1, "two", 3, "four"]; // Array that accepts either all numbers or all strings let data: number[] | string[] = [1, 2, 3]; data = ["a", "b", "c"]; // Valid reassignment
Literal Union Types
A union of specific literal values restricts a variable to only those exact values. This works like a custom enum using string or number literals.
type Direction = "north" | "south" | "east" | "west"; type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; type Size = "small" | "medium" | "large" | "xl"; let heading: Direction = "north"; heading = "east"; // Valid heading = "up"; // Error: '"up"' not assignable to type 'Direction' let roll: DiceRoll = 6; roll = 7; // Error: 7 not in literal union let tshirtSize: Size = "medium";
Union in Object Properties
type Notification = {
id: number;
message: string;
type: "info" | "warning" | "error" | "success";
timestamp: Date | string;
};
let alert: Notification = {
id: 1,
message: "Your order has been placed.",
type: "success",
timestamp: new Date()
};
let legacyAlert: Notification = {
id: 2,
message: "Server maintenance at midnight.",
type: "warning",
timestamp: "2025-06-10T00:00:00" // string format also accepted
};
Discriminated Unions
A discriminated union uses a common property (the discriminant) to tell types apart. TypeScript uses the discriminant value to narrow to the exact type in each branch.
Shape diagram:
+------------+ +------------+ +------------+
| Circle | | Rectangle | | Triangle |
| kind:"circ"| | kind:"rect"| | kind:"tri" |
| radius | | width | | base |
+------------+ | height | | height |
+------------+ +------------+
type Circle = {
kind: "circle";
radius: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Triangle = {
kind: "triangle";
base: number;
height: number;
};
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius * shape.radius;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
}
}
console.log(getArea({ kind: "circle", radius: 5 }));
// Output: 78.53...
console.log(getArea({ kind: "rectangle", width: 4, height: 6 }));
// Output: 24
console.log(getArea({ kind: "triangle", base: 8, height: 5 }));
// Output: 20
Practical Example
// API response handler
type ApiSuccess = {
status: "success";
data: string[];
count: number;
};
type ApiError = {
status: "error";
errorCode: number;
message: string;
};
type ApiResponse = ApiSuccess | ApiError;
function handleResponse(response: ApiResponse): void {
if (response.status === "success") {
console.log("Data received: " + response.count + " items");
response.data.forEach(item => console.log(" - " + item));
} else {
console.log("Error " + response.errorCode + ": " + response.message);
}
}
handleResponse({ status: "success", data: ["TypeScript", "React"], count: 2 });
// Data received: 2 items
// - TypeScript
// - React
handleResponse({ status: "error", errorCode: 404, message: "Not Found" });
// Error 404: Not Found
