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.