Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
LoginSign Up
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Union Intersection Types
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

Union and Intersection Types

Combining types with | and & operators

Union and intersection types are powerful tools for combining existing types into new ones. Union types (|) represent values that can be one of several types, while intersection types (&) combine multiple types into one. These features give you incredible flexibility in modeling complex data structures. Let's master both!

Union Types

A union type describes a value that can be one of several types. Use the | operator to create unions. Think of it as "OR" logic for types.

Basic Union Types

union-basics.ts
// Simple union type
let value: string | number;

value = "hello";   // ✅ Works (string)
value = 42;        // ✅ Works (number)
// value = true;   // ❌ Error: boolean not in union

// Union with multiple types
let id: string | number | boolean;

id = "user-123";   // ✅ string
id = 123;          // ✅ number
id = true;         // ✅ boolean

// Function with union parameter
function printId(id: string | number): void {
  console.log(`ID: ${id}`);
}

printId(123);        // ✅ Works
printId("abc-123");  // ✅ Works
// printId(true);    // ❌ Error

// Union type alias
type ID = string | number;

let userId: ID = 123;
let productId: ID = "prod-456";

// Array of union types
let mixed: (string | number)[] = [1, "two", 3, "four"];
console.log(mixed);  // [1, "two", 3, "four"]

// Union with null/undefined
let nullable: string | null = null;
let optional: number | undefined = undefined;

nullable = "value";   // ✅ Works
optional = 42;        // ✅ Works

Working with Union Types

working-with-unions.ts
// Only common properties are accessible
type Dog = {
  name: string;
  breed: string;
  bark(): void;
};

type Cat = {
  name: string;
  color: string;
  meow(): void;
};

type Pet = Dog | Cat;

function getPetName(pet: Pet): string {
  // Can access 'name' - exists on both types
  return pet.name;  // ✅ Works
  
  // Cannot access 'breed' - only on Dog
  // return pet.breed;  // ❌ Error
  
  // Cannot access 'color' - only on Cat
  // return pet.color;  // ❌ Error
}

// Must narrow type to access specific properties
function describePet(pet: Pet): void {
  console.log(`Name: ${pet.name}`);  // ✅ Common property
  
  // Type narrowing with 'in' operator
  if ("breed" in pet) {
    // TypeScript knows pet is Dog here
    console.log(`Breed: ${pet.breed}`);
    pet.bark();
  } else {
    // TypeScript knows pet is Cat here
    console.log(`Color: ${pet.color}`);
    pet.meow();
  }
}

const dog: Dog = {
  name: "Max",
  breed: "Golden Retriever",
  bark() { console.log("Woof!"); }
};

const cat: Cat = {
  name: "Whiskers",
  color: "Orange",
  meow() { console.log("Meow!"); }
};

describePet(dog);  // "Name: Max" "Breed: Golden Retriever" "Woof!"
describePet(cat);  // "Name: Whiskers" "Color: Orange" "Meow!"

Union Type Structure

▼
Visitor (Union Type)union

Can be EITHER User OR Guest

type User = { name: string; age: number };
type Guest = { sessionId: string; temporary: boolean };
type Visitor = User | Guest;
User{ name: string; age: number }

Registered user with name and age

Guest{ sessionId: string; temporary: boolean }

Temporary guest with session ID

Type Narrowing

Type narrowing is how you work with union types safely. You narrow a union type down to a specific type using type guards.

typeof Type Guards

typeof-guards.ts
// Using typeof for primitive types
function formatValue(value: string | number): string {
  if (typeof value === "string") {
    // TypeScript knows value is string here
    return value.toUpperCase();
  }
  
  // TypeScript knows value is number here
  return value.toFixed(2);
}

console.log(formatValue("hello"));   // "HELLO"
console.log(formatValue(123.456));   // "123.46"

// Multiple type checks
function process(value: string | number | boolean): string {
  if (typeof value === "string") {
    return `String: ${value}`;
  }
  
  if (typeof value === "number") {
    return `Number: ${value.toFixed(2)}`;
  }
  
  // TypeScript knows value is boolean here
  return `Boolean: ${value ? "Yes" : "No"}`;
}

console.log(process("hello"));   // "String: hello"
console.log(process(42));         // "Number: 42.00"
console.log(process(true));       // "Boolean: Yes"

in Operator Type Guards

in-operator-guards.ts
// Using 'in' to check for properties
type User = {
  name: string;
  email: string;
  role: "user";
};

type Admin = {
  name: string;
  email: string;
  role: "admin";
  permissions: string[];
};

type Account = User | Admin;

function getAccountInfo(account: Account): void {
  console.log(`Name: ${account.name}`);
  console.log(`Email: ${account.email}`);
  console.log(`Role: ${account.role}`);
  
  // Check if 'permissions' property exists
  if ("permissions" in account) {
    // TypeScript knows account is Admin here
    console.log(`Permissions: ${account.permissions.join(", ")}`);
  } else {
    // TypeScript knows account is User here
    console.log("Regular user - no special permissions");
  }
}

const user: User = {
  name: "Yusuf Ibrahim",
  email: "yusuf@example.com",
  role: "user"
};

const admin: Admin = {
  name: "Amina Hassan",
  email: "amina@example.com",
  role: "admin",
  permissions: ["read", "write", "delete"]
};

getAccountInfo(user);
getAccountInfo(admin);

instanceof Type Guards

instanceof-guards.ts
// Using instanceof with classes
class Dog {
  constructor(public name: string, public breed: string) {}
  
  bark(): void {
    console.log("Woof! Woof!");
  }
}

class Cat {
  constructor(public name: string, public color: string) {}
  
  meow(): void {
    console.log("Meow!");
  }
}

type Pet = Dog | Cat;

function playWithPet(pet: Pet): void {
  console.log(`Playing with ${pet.name}`);
  
  if (pet instanceof Dog) {
    // TypeScript knows pet is Dog here
    console.log(`Breed: ${pet.breed}`);
    pet.bark();
  } else {
    // TypeScript knows pet is Cat here
    console.log(`Color: ${pet.color}`);
    pet.meow();
  }
}

const dog = new Dog("Max", "Labrador");
const cat = new Cat("Whiskers", "Gray");

playWithPet(dog);
playWithPet(cat);

Custom Type Guards

custom-type-guards.ts
// Create custom type guard functions
type Fish = {
  swim(): void;
};

type Bird = {
  fly(): void;
};

type Animal = Fish | Bird;

// Custom type guard with 'is' keyword
function isFish(animal: Animal): animal is Fish {
  return (animal as Fish).swim !== undefined;
}

function isBird(animal: Animal): animal is Bird {
  return (animal as Bird).fly !== undefined;
}

// Using custom type guards
function moveAnimal(animal: Animal): void {
  if (isFish(animal)) {
    // TypeScript knows animal is Fish here
    animal.swim();
  } else {
    // TypeScript knows animal is Bird here
    animal.fly();
  }
}

const fish: Fish = {
  swim() { console.log("Swimming..."); }
};

const bird: Bird = {
  fly() { console.log("Flying..."); }
};

moveAnimal(fish);  // "Swimming..."
moveAnimal(bird);  // "Flying..."

// More complex custom type guard
type ApiSuccess = {
  status: "success";
  data: any;
};

type ApiError = {
  status: "error";
  message: string;
};

type ApiResponse = ApiSuccess | ApiError;

function isSuccess(response: ApiResponse): response is ApiSuccess {
  return response.status === "success";
}

function handleResponse(response: ApiResponse): void {
  if (isSuccess(response)) {
    // TypeScript knows response is ApiSuccess
    console.log("Data:", response.data);
  } else {
    // TypeScript knows response is ApiError
    console.error("Error:", response.message);
  }
}

Discriminated Unions

Discriminated unions (also called tagged unions) use a common property to distinguish between union members. This is one of the most powerful patterns in TypeScript.

discriminated-unions.ts
// Shape types with discriminant property 'kind'
type Circle = {
  kind: "circle";
  radius: number;
};

type Square = {
  kind: "square";
  size: number;
};

type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
};

type Shape = Circle | Square | Rectangle;

// TypeScript uses 'kind' to narrow the type
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // TypeScript knows shape is Circle here
      return Math.PI * shape.radius ** 2;
    
    case "square":
      // TypeScript knows shape is Square here
      return shape.size ** 2;
    
    case "rectangle":
      // TypeScript knows shape is Rectangle here
      return shape.width * shape.height;
  }
}

const circle: Circle = { kind: "circle", radius: 5 };
const square: Square = { kind: "square", size: 10 };
const rectangle: Rectangle = { kind: "rectangle", width: 20, height: 15 };

console.log(`Circle area: ${getArea(circle)}`);           // 78.54
console.log(`Square area: ${getArea(square)}`);           // 100
console.log(`Rectangle area: ${getArea(rectangle)}`);     // 300

Practical Example: State Management

state-management.ts
// Loading states with discriminated union
type LoadingState = {
  status: "loading";
};

type SuccessState<T> = {
  status: "success";
  data: T;
};

type ErrorState = {
  status: "error";
  error: string;
};

type DataState<T> = LoadingState | SuccessState<T> | ErrorState;

// User data example
type User = {
  id: number;
  name: string;
  email: string;
};

function renderUserProfile(state: DataState<User>): string {
  switch (state.status) {
    case "loading":
      return "Loading user profile...";
    
    case "success":
      // TypeScript knows state has 'data' property
      return `Welcome, ${state.data.name} (${state.data.email})`;
    
    case "error":
      // TypeScript knows state has 'error' property
      return `Error: ${state.error}`;
  }
}

// Different states
const loading: DataState<User> = {
  status: "loading"
};

const success: DataState<User> = {
  status: "success",
  data: {
    id: 1,
    name: "Yusuf Ibrahim",
    email: "yusuf@example.com"
  }
};

const error: DataState<User> = {
  status: "error",
  error: "Failed to load user"
};

console.log(renderUserProfile(loading));   // "Loading user profile..."
console.log(renderUserProfile(success));   // "Welcome, Yusuf Ibrahim..."
console.log(renderUserProfile(error));     // "Error: Failed to load user"

Actions Pattern

actions-pattern.ts
// Redux-style action types
type LoginAction = {
  type: "LOGIN";
  payload: {
    username: string;
    password: string;
  };
};

type LogoutAction = {
  type: "LOGOUT";
};

type UpdateProfileAction = {
  type: "UPDATE_PROFILE";
  payload: {
    name: string;
    email: string;
  };
};

type Action = LoginAction | LogoutAction | UpdateProfileAction;

// Reducer function with exhaustive checking
function reducer(action: Action): void {
  switch (action.type) {
    case "LOGIN":
      // TypeScript knows action has payload with username and password
      console.log(`Logging in: ${action.payload.username}`);
      break;
    
    case "LOGOUT":
      // TypeScript knows action has no payload
      console.log("Logging out");
      break;
    
    case "UPDATE_PROFILE":
      // TypeScript knows action has payload with name and email
      console.log(`Updating profile: ${action.payload.name}`);
      break;
    
    default:
      // Exhaustive check - ensures all cases handled
      const exhaustiveCheck: never = action;
      throw new Error(`Unhandled action: ${exhaustiveCheck}`);
  }
}

// Dispatch actions
reducer({ 
  type: "LOGIN", 
  payload: { username: "yusuf", password: "secret" } 
});

reducer({ type: "LOGOUT" });

reducer({ 
  type: "UPDATE_PROFILE", 
  payload: { name: "Yusuf Ibrahim", email: "yusuf@example.com" } 
});

Intersection Types

An intersection type combines multiple types into one. The resulting type has all properties from all types. Use the & operator to create intersections. Think of it as "AND" logic for types.

Basic Intersection Types

intersection-basics.ts
// Combining types with intersection
type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: number;
  department: string;
};

// Intersection requires ALL properties from both types
type WorkingPerson = Person & Employee;

const employee: WorkingPerson = {
  name: "Yusuf Ibrahim",
  age: 25,
  employeeId: 1001,
  department: "Engineering"
};

console.log(employee);

// Missing any property causes error
// const invalid: WorkingPerson = {
//   name: "Amina",
//   age: 30,
//   employeeId: 1002
//   // ❌ Error: Missing 'department'
// };

// Three-way intersection
type Contact = {
  email: string;
  phone: string;
};

type FullProfile = Person & Employee & Contact;

const profile: FullProfile = {
  name: "Chidi Okafor",
  age: 28,
  employeeId: 1003,
  department: "Sales",
  email: "chidi@example.com",
  phone: "+234-805-987-6543"
};

console.log(profile);

Intersection with Interfaces

intersection-interfaces.ts
// Intersecting interfaces
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface Identifiable {
  id: number;
}

interface Auditable {
  createdBy: number;
  updatedBy: number;
}

// Combine multiple interfaces
type DatabaseEntity = Timestamped & Identifiable & Auditable;

const record: DatabaseEntity = {
  id: 1,
  createdAt: new Date("2024-01-01"),
  updatedAt: new Date("2024-12-25"),
  createdBy: 101,
  updatedBy: 102
};

// Use with additional properties
type Product = DatabaseEntity & {
  name: string;
  price: number;
};

const product: Product = {
  id: 1,
  name: "Laptop",
  price: 450000,
  createdAt: new Date(),
  updatedAt: new Date(),
  createdBy: 1,
  updatedBy: 1
};

console.log(product);

Intersection with Function Types

intersection-functions.ts
// Intersecting function types
type Logger = {
  log(message: string): void;
};

type ErrorHandler = {
  handleError(error: Error): void;
};

type Service = Logger & ErrorHandler;

const service: Service = {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  },
  
  handleError(error: Error): void {
    console.error(`[ERROR] ${error.message}`);
  }
};

service.log("Service started");
service.handleError(new Error("Something went wrong"));

// Intersection with callable signature
type Callable = {
  (): void;
};

type WithProperties = {
  value: number;
};

type CallableWithProps = Callable & WithProperties;

const func: CallableWithProps = Object.assign(
  () => console.log("Called!"),
  { value: 42 }
);

func();                  // "Called!"
console.log(func.value); // 42

Union vs Intersection

FeatureUnion (A | B)Intersection (A & B)
MeaningOR - can be A OR BAND - must be A AND B
PropertiesOnly common properties accessibleAll properties from both types
FlexibilityMore flexible (less strict)Less flexible (more strict)
Use caseDifferent possible typesCombining features/mixins
Type narrowingUsually requiredNot required
union-vs-intersection-example.ts
type HasName = { name: string };
type HasAge = { age: number };

// UNION: Can be HasName OR HasAge (or both)
type UnionType = HasName | HasAge;

const union1: UnionType = { name: "Yusuf" };              // ✅ Only HasName
const union2: UnionType = { age: 25 };                     // ✅ Only HasAge
const union3: UnionType = { name: "Amina", age: 30 };      // ✅ Both

// Can only access properties that exist on BOTH types
function printUnion(value: UnionType): void {
  // console.log(value.name);  // ❌ Error: name doesn't exist on HasAge
  // console.log(value.age);   // ❌ Error: age doesn't exist on HasName
  
  // Must narrow type first
  if ("name" in value) {
    console.log(value.name);  // ✅ Works after narrowing
  }
}

// INTERSECTION: Must have properties from BOTH HasName AND HasAge
type IntersectionType = HasName & HasAge;

const inter1: IntersectionType = { name: "Yusuf", age: 25 };  // ✅ Has both
// const inter2: IntersectionType = { name: "Amina" };        // ❌ Missing age
// const inter3: IntersectionType = { age: 30 };              // ❌ Missing name

// Can access ALL properties without narrowing
function printIntersection(value: IntersectionType): void {
  console.log(value.name);  // ✅ Always available
  console.log(value.age);   // ✅ Always available
}

printIntersection({ name: "Chidi", age: 28 });

Practical Examples

Example 1: API Response Types

api-response-types.ts
// Base response structure
type BaseResponse = {
  timestamp: Date;
  requestId: string;
};

// Success response
type SuccessResponse<T> = BaseResponse & {
  status: "success";
  data: T;
};

// Error response
type ErrorResponse = BaseResponse & {
  status: "error";
  error: {
    code: string;
    message: string;
  };
};

// Union of possible responses
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;

// User type
type User = {
  id: number;
  name: string;
  email: string;
};

// Handle API response
function handleUserResponse(response: ApiResponse<User>): void {
  // Common properties always available
  console.log(`Request ID: ${response.requestId}`);
  console.log(`Timestamp: ${response.timestamp}`);
  
  // Discriminated union with status
  if (response.status === "success") {
    // TypeScript knows response has 'data'
    console.log(`User: ${response.data.name}`);
    console.log(`Email: ${response.data.email}`);
  } else {
    // TypeScript knows response has 'error'
    console.error(`Error ${response.error.code}: ${response.error.message}`);
  }
}

// Mock responses
const success: ApiResponse<User> = {
  status: "success",
  timestamp: new Date(),
  requestId: "req-123",
  data: {
    id: 1,
    name: "Yusuf Ibrahim",
    email: "yusuf@example.com"
  }
};

const error: ApiResponse<User> = {
  status: "error",
  timestamp: new Date(),
  requestId: "req-124",
  error: {
    code: "USER_NOT_FOUND",
    message: "User with given ID not found"
  }
};

handleUserResponse(success);
handleUserResponse(error);

Example 2: Payment Methods

payment-methods.ts
// Different payment method types
type CashPayment = {
  method: "cash";
  amount: number;
  currency: string;
};

type CardPayment = {
  method: "card";
  amount: number;
  currency: string;
  cardNumber: string;
  cardHolder: string;
};

type BankTransfer = {
  method: "bank_transfer";
  amount: number;
  currency: string;
  accountNumber: string;
  bankName: string;
};

type MobileMoney = {
  method: "mobile_money";
  amount: number;
  currency: string;
  phoneNumber: string;
  provider: string;
};

// Union of all payment methods
type Payment = CashPayment | CardPayment | BankTransfer | MobileMoney;

// Process payment based on method
function processPayment(payment: Payment): string {
  // Common properties available
  console.log(`Processing ${payment.currency}${payment.amount}`);
  
  switch (payment.method) {
    case "cash":
      return `Cash payment of ${payment.currency}${payment.amount} received`;
    
    case "card":
      return `Card payment from ${payment.cardHolder} (****${payment.cardNumber.slice(-4)})`;
    
    case "bank_transfer":
      return `Bank transfer from ${payment.bankName} account ${payment.accountNumber}`;
    
    case "mobile_money":
      return `Mobile money from ${payment.provider}: ${payment.phoneNumber}`;
    
    default:
      const exhaustive: never = payment;
      throw new Error(`Unhandled payment method: ${exhaustive}`);
  }
}

// Different payment instances
const cash: Payment = {
  method: "cash",
  amount: 10000,
  currency: "₦"
};

const card: Payment = {
  method: "card",
  amount: 50000,
  currency: "₦",
  cardNumber: "1234567812345678",
  cardHolder: "Yusuf Ibrahim"
};

const transfer: Payment = {
  method: "bank_transfer",
  amount: 100000,
  currency: "₦",
  accountNumber: "0123456789",
  bankName: "GTBank"
};

const mobile: Payment = {
  method: "mobile_money",
  amount: 5000,
  currency: "₦",
  phoneNumber: "+234-803-123-4567",
  provider: "MTN"
};

console.log(processPayment(cash));
console.log(processPayment(card));
console.log(processPayment(transfer));
console.log(processPayment(mobile));

Example 3: Mixin Pattern with Intersection

mixin-pattern.ts
// Base functionality
type Printable = {
  print(): void;
};

type Serializable = {
  toJSON(): string;
};

type Comparable = {
  equals(other: any): boolean;
};

// Combine functionalities with intersection
type FullFeatured = Printable & Serializable & Comparable;

// Implement all features
class Document implements FullFeatured {
  constructor(
    public title: string,
    public content: string
  ) {}
  
  print(): void {
    console.log(`=== ${this.title} ===`);
    console.log(this.content);
  }
  
  toJSON(): string {
    return JSON.stringify({
      title: this.title,
      content: this.content
    });
  }
  
  equals(other: Document): boolean {
    return this.title === other.title &&
           this.content === other.content;
  }
}

const doc1 = new Document("Report", "This is the report content");
const doc2 = new Document("Report", "This is the report content");
const doc3 = new Document("Letter", "This is a letter");

doc1.print();
// === Report ===
// This is the report content

console.log(doc1.toJSON());
// {"title":"Report","content":"This is the report content"}

console.log(doc1.equals(doc2));  // true
console.log(doc1.equals(doc3));  // false

Test Your Knowledge

Type Safety Check

Which type combination is valid?

type A = { name: string; age: number };
type B = { email: string; phone: string };

// Option 1
type Union = A | B;
const user1: Union = { name: "Yusuf", age: 25 };

// Option 2
type Intersection = A & B;
const user2: Intersection = { name: "Yusuf", age: 25 };

// Option 3
type Intersection = A & B;
const user3: Intersection = { 
  name: "Yusuf", 
  age: 25, 
  email: "yusuf@example.com",
  phone: "+234-803-123-4567"
};

// Option 4
type Union = A | B;
const user4: Union = { name: "Yusuf" };

Common TypeScript Error

Code with Error
// Trying to access property without type narrowing
type User = { name: string; age: number };
type Guest = { sessionId: string; temporary: boolean };

type Visitor = User | Guest;

function displayVisitor(visitor: Visitor) {
  // Error: Property 'name' does not exist on type 'Visitor'
  console.log(visitor.name);  
  
  // Error: Property 'sessionId' does not exist on type 'Visitor'
  console.log(visitor.sessionId);
}

❌ Property 'name' does not exist on type 'Visitor'. Property 'sessionId' does not exist on type 'Visitor'.

What's Wrong?

With union types, you can only access properties that exist on ALL types in the union. Since 'name' doesn't exist on Guest and 'sessionId' doesn't exist on User, TypeScript prevents access.

Corrected Code
// Fixed: Use type narrowing
type User = { name: string; age: number };
type Guest = { sessionId: string; temporary: boolean };

type Visitor = User | Guest;

function displayVisitor(visitor: Visitor) {
  // Type guard to narrow the type
  if ("name" in visitor) {
    // TypeScript knows visitor is User here
    console.log(`User: ${visitor.name}, Age: ${visitor.age}`);
  } else {
    // TypeScript knows visitor is Guest here
    console.log(`Guest: ${visitor.sessionId}, Temporary: ${visitor.temporary}`);
  }
}

// Usage
displayVisitor({ name: "Yusuf", age: 25 });
displayVisitor({ sessionId: "abc123", temporary: true });

Solution: Use type narrowing to determine which type you have

Best Practices

  1. Use discriminated unions for better type narrowing
  2. Prefer union types for "one of several" scenarios
  3. Use intersection types for combining features/mixins
  4. Always narrow union types before accessing specific properties
  5. Use never for exhaustive checks in switch statements
  6. Keep discriminant properties simple (strings or numbers)
  7. Document complex unions with comments
  8. Avoid deep nesting of unions and intersections
best-practices-example.ts
// ✅ Good: Discriminated union with clear discriminant
type Success = { status: "success"; data: string };
type Error = { status: "error"; message: string };
type Result = Success | Error;

function handleResult(result: Result): void {
  if (result.status === "success") {
    console.log(result.data);
  } else {
    console.error(result.message);
  }
}

// ✅ Good: Intersection for combining features
type Timestamped = { createdAt: Date };
type Identifiable = { id: number };
type Entity = Timestamped & Identifiable;

// ✅ Good: Exhaustive checking with never
type Action = { type: "login" } | { type: "logout" };

function reducer(action: Action): void {
  switch (action.type) {
    case "login":
      return;
    case "logout":
      return;
    default:
      const exhaustive: never = action;
      throw new Error(`Unhandled: ${exhaustive}`);
  }
}

// ❌ Avoid: Non-discriminated unions (harder to narrow)
type Bad = { name: string } | { title: string };
// Hard to tell which type you have

// ❌ Avoid: Deep nesting
type Messy = (A | B) & (C | D) & (E | F);
// Hard to understand and maintain

// ✅ Better: Break into smaller types
type Combined1 = A | B;
type Combined2 = C | D;
type Combined3 = E | F;
type Better = Combined1 & Combined2 & Combined3;

Key Takeaways

  • Union types (|) mean "OR" - can be one of several types
  • Intersection types (&) mean "AND" - must have all properties
  • Union types require type narrowing to access specific properties
  • Intersection types give immediate access to all properties
  • Discriminated unions use a common property for type narrowing
  • Use typeof, in, instanceof for type guards
  • Custom type guards use is keyword
  • Exhaustive checking with never ensures all cases are handled
  • Union types are more flexible, intersection types are more strict
  • Combine union and intersection for powerful type definitions

What's Next?

Fantastic! You've mastered union and intersection types, one of TypeScript's most powerful features. Next, we'll explore Type Guards and Type Assertions in even more depth. You'll learn advanced narrowing techniques, type predicates, assertion functions, and how to safely work with types at runtime. These skills will make you a TypeScript expert!

Get ready to become a type narrowing master!

Mastering TypeScript union and intersection types! Check this out!

Previous
Function Overloading
Next
Type Guards and Narrowing

Never Miss a New TypeScript Tutorial

Join 1,000+ developers learning TypeScript step-by-step. Get new tutorials, type safety tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo