Module 7: Union and Intersection Types
TypeScript's union and intersection types allow you to combine types in powerful ways, creating flexible and precise type definitions.
1. Union Types
Union types represent values that can be one of several types.
Basic Union
let value: string | number;
value = "Hello"; // ✅ OK
value = 42; // ✅ OK
// value = true; // ❌ Error: boolean not allowed
Union with Multiple Types
type Status = "success" | "error" | "pending";
let currentStatus: Status = "success"; // ✅ OK
// currentStatus = "failed"; // ❌ Error
2. Union Type Functions
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
} else {
return value.toFixed(2);
}
}
console.log(formatValue("hello")); // "HELLO"
console.log(formatValue(42.567)); // "42.57"
Multiple Parameter Types
function printId(id: string | number): void {
console.log(`ID: ${id}`);
}
printId(101); // "ID: 101"
printId("ABC123"); // "ID: ABC123"
3. Union with Arrays
let mixedArray: (string | number)[] = [1, "two", 3, "four"];
// Array of numbers OR array of strings
let arrayOfEither: number[] | string[] = [1, 2, 3];
arrayOfEither = ["a", "b", "c"]; // ✅ OK
// arrayOfEither = [1, "two"]; // ❌ Error
4. Discriminated Unions (Tagged Unions)
Use a common property to distinguish between union members.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
let circle: Circle = { kind: "circle", radius: 5 };
console.log(getArea(circle)); // 78.53981633974483
Discriminated unions are excellent for representing state machines and API responses.
5. Union Type Narrowing
TypeScript narrows union types based on control flow.
function processValue(value: string | number | boolean): void {
if (typeof value === "string") {
console.log(value.toUpperCase()); // TypeScript knows it's string
} else if (typeof value === "number") {
console.log(value.toFixed(2)); // TypeScript knows it's number
} else {
console.log(value ? "true" : "false"); // TypeScript knows it's boolean
}
}
6. Intersection Types
Intersection types combine multiple types into one.
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: number;
department: string;
}
type EmployeePerson = Person & Employee;
let employee: EmployeePerson = {
name: "Alice",
age: 30,
employeeId: 1001,
department: "IT"
};
7. Intersection with Type Aliases
type Printable = {
print(): void;
};
type Loggable = {
log(): void;
};
type PrintableLoggable = Printable & Loggable;
let obj: PrintableLoggable = {
print() {
console.log("Printing...");
},
log() {
console.log("Logging...");
}
};
8. Mixing Unions and Intersections
type NetworkState =
| { state: "loading" }
| { state: "success"; data: string }
| { state: "error"; error: Error };
function handleNetworkState(state: NetworkState): void {
switch (state.state) {
case "loading":
console.log("Loading...");
break;
case "success":
console.log("Data:", state.data);
break;
case "error":
console.log("Error:", state.error.message);
break;
}
}
9. Union with Null/Undefined
function greet(name: string | null | undefined): string {
if (name === null || name === undefined) {
return "Hello, Guest!";
}
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet(null)); // "Hello, Guest!"
console.log(greet(undefined)); // "Hello, Guest!"
10. Type Guards with Unions
typeof Guard
function double(value: string | number): string | number {
if (typeof value === "string") {
return value + value;
}
return value * 2;
}
instanceof Guard
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
in Operator
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish): void {
if ("fly" in animal) {
animal.fly();
} else {
animal.swim();
}
}
11. Real-World Example: API Response
interface SuccessResponse<T> {
status: "success";
data: T;
}
interface ErrorResponse {
status: "error";
error: string;
}
type APIResponse<T> = SuccessResponse<T> | ErrorResponse;
function handleResponse<T>(response: APIResponse<T>): void {
if (response.status === "success") {
console.log("Data:", response.data);
} else {
console.error("Error:", response.error);
}
}
// Usage
let userResponse: APIResponse<{ id: number; name: string }> = {
status: "success",
data: { id: 1, name: "Alice" }
};
handleResponse(userResponse);
12. Intersection with Mixins
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
class Timestamped {
timestamp = new Date();
}
class Tagged {
tag = "default";
}
class Document {}
interface Document extends Timestamped, Tagged {}
applyMixins(Document, [Timestamped, Tagged]);
let doc = new Document();
console.log(doc.timestamp);
console.log(doc.tag);
Key Takeaways
✅ Union types (A | B) represent "either A or B"
✅ Intersection types (A & B) represent "both A and B"
✅ Discriminated unions use a common property for type narrowing
✅ Type guards help narrow union types safely
✅ Combine unions and intersections for complex types
✅ Excellent for modeling API responses and state
Practice Exercises
Exercise 1: Payment Method
type PaymentMethod =
| { type: "credit-card"; cardNumber: string; cvv: string }
| { type: "paypal"; email: string }
| { type: "bitcoin"; walletAddress: string };
function processPayment(payment: PaymentMethod): void {
switch (payment.type) {
case "credit-card":
console.log(`Processing card: ${payment.cardNumber}`);
break;
case "paypal":
console.log(`Processing PayPal: ${payment.email}`);
break;
case "bitcoin":
console.log(`Processing Bitcoin: ${payment.walletAddress}`);
break;
}
}
Exercise 2: User Roles
interface Admin {
role: "admin";
permissions: string[];
}
interface User {
role: "user";
email: string;
}
type Person = Admin | User;
function getAccessLevel(person: Person): string {
if (person.role === "admin") {
return `Admin with ${person.permissions.length} permissions`;
}
return `User: ${person.email}`;
}
Exercise 3: Shape Calculator
Create a discriminated union for different shapes and calculate their areas.
Next Steps
In Module 8, we'll dive deep into Type Guards and Narrowing to learn advanced techniques for working with union types safely.