Skip to main content

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
Best Practice

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.