Skip to content
JL

Home

About

Blog

Contact

Shop

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Intersection Types: Combining Types in TypeScript

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Literal Types and Const Assertions in TypeScript
TypeScript
7m
Apr 19, 2026

Literal Types and Const Assertions in TypeScript

A practical guide to TypeScript literal types and as const — narrowing values to exact possibilities, building type-safe enumerations, and preserving tuple and object shapes.

#Literal Types#Const Assertions+5
Union Types: Multiple Possibilities in TypeScript
TypeScript
7m
Apr 5, 2026

Union Types: Multiple Possibilities in TypeScript

A practical guide to TypeScript union types — combining multiple types with OR logic, accessing shared properties, and narrowing unions safely at runtime.

#Union Types#Type Narrowing+5
Mastering TypeScript Utility Types
TypeScript
9m
Feb 1, 2026

Mastering TypeScript Utility Types

TypeScript provides powerful built-in utility types that help you transform existing types without rewriting them. This guide covers the most essential utility types and how to use them effectively in your projects.

#TypeScript#Utility Types+4
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • What Are Intersection Types
  • The Ampersand Syntax
  • Combining Object Types
  • Intersection Types vs Interface Extension
  • Mixin Patterns
  • Real-World Patterns
  • Best Practices
  • Key Takeaways
TypeScript

Intersection Types: Combining Types in TypeScript

April 12, 2026•5 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript intersection types combining multiple types
Intersection Types: Combining Types in TypeScript

What Are Intersection Types

Where union types express OR logic — "this value is A or B" — intersection types express AND logic: "this value is both A and B." An intersection type requires a value to satisfy every member of the type simultaneously.

Think of it like a job posting. A "Senior TypeScript Developer" role might require both FrontendSkills and BackendSkills. The candidate must have all the properties of both types — not one or the other. Intersection types model exactly this: the combined requirements of multiple independent types.

Intersection Types Express AND Logic

A type A & B means a value must satisfy both type A and type B. The resulting type has all the properties of A plus all the properties of B. If A and B share a property name, the types of that property are themselves intersected.

The Ampersand Syntax

Intersection types use the & operator:

type HasName = { name: string };
type HasAge = { age: number };
 
type Person = HasName & HasAge;
 
const alice: Person = {
  name: "Alice",
  age: 30,
};
 
// Missing a property from either type causes an error
const bob: Person = {
  name: "Bob",
  // Error: property 'age' is missing in type '{ name: string }'
};

The resulting Person type requires both name and age. Omitting either causes a compile error.

Combining Object Types

Intersection types are most useful when combining object types. The resulting type has every property from every member:

type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};
 
type SoftDeletable = {
  deletedAt: Date | null;
};
 
type Identifiable = {
  id: string;
};
 
// Combine all three into a base entity type
type Entity = Identifiable & Timestamped & SoftDeletable;
 
const user: Entity = {
  id: "usr-001",
  createdAt: new Date(),
  updatedAt: new Date(),
  deletedAt: null,
};

This is a common pattern for building up base types from focused, reusable pieces. Each constituent type (Timestamped, SoftDeletable, Identifiable) has a single responsibility, and Entity composes them all.

Conflicting Primitive Properties Produce never

If two types in an intersection have the same property name with incompatible types, the result is never — a type no value can satisfy. For example, { id: string } & { id: number } produces { id: never }, making any value of that type impossible to construct. Design your intersected types to avoid overlapping property names, or ensure they overlap with the same type.

Intersection Types vs Interface Extension

You can achieve similar results with interface extension using extends. Both approaches combine types, but they differ in flexibility and error reporting:

// Interface extension
interface Animal {
  name: string;
}
 
interface Dog extends Animal {
  barks: boolean;
}
 
// Intersection type
type Animal2 = { name: string };
type Dog2 = Animal2 & { barks: boolean };

The practical differences:

FeatureextendsIntersection &
Works withInterfaces and classesAny type
Error messagesClearer, points to the interfaceCan be verbose
Conflicting propertiesCompile errorProduces never
Multiple sourcesextends A, BA & B & C

Use extends when working with interfaces in a class hierarchy. Use & when you need to combine type aliases, third-party types you don't control, or types built dynamically with generics.

Mixin Patterns

Intersection types enable mixin patterns — attaching reusable behaviours to classes without inheritance chains:

type Serializable = {
  serialize(): string;
  deserialize(data: string): void;
};
 
type Loggable = {
  log(message: string): void;
};
 
type Validatable = {
  validate(): boolean;
};
 
// A type that requires all three capabilities
type FullService = Serializable & Loggable & Validatable;
 
function processService(service: FullService): void {
  if (!service.validate()) {
    service.log("Validation failed");
    return;
  }
 
  const data = service.serialize();
  service.log(`Serialized: ${data}`);
}

The processService function doesn't care how a service implements its behaviours — it only cares that the incoming object satisfies all three contracts. This is composition over inheritance in TypeScript form.

Real-World Patterns

A common use case is augmenting third-party types you can't modify directly. Suppose a library gives you a BaseRequest type but you need to add authentication fields for your application:

// From a library you don't control
type BaseRequest = {
  url: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
  headers: Record<string, string>;
};
 
// Your application's additions
type AuthenticatedRequest = BaseRequest & {
  userId: string;
  sessionToken: string;
  permissions: string[];
};
 
// Handler that requires full auth context
function handleAuthenticatedRequest(req: AuthenticatedRequest): void {
  console.log(`User ${req.userId} requesting ${req.method} ${req.url}`);
 
  if (!req.permissions.includes("read")) {
    throw new Error("Permission denied");
  }
 
  // Process the request...
}
 
// Building a request object
const request: AuthenticatedRequest = {
  url: "/api/data",
  method: "GET",
  headers: { "Content-Type": "application/json" },
  userId: "usr-001",
  sessionToken: "tok-abc",
  permissions: ["read", "write"],
};

Intersection Types and Generics

Intersection types become especially powerful when combined with generics. You can write functions that accept a base type augmented with additional requirements: function process<T>(item: T & Identifiable): void — T must have whatever it has, plus an id field. This pattern appears frequently in generic repository and service layers.

Best Practices

Use intersection types when:

  • Composing reusable "trait" types that can be mixed into other types
  • Augmenting a third-party type without modifying it
  • Building generic constraints that combine multiple requirements
  • Combining type aliases where extends isn't available

Avoid intersection types when:

  • The types have overlapping property names with different types (produces never)
  • You're working within a class hierarchy (use extends instead)
  • The intent is OR logic (use | instead)
// Good — non-overlapping properties, clean composition
type AdminUser = User & { adminSince: Date; permissions: string[] };
 
// Problematic — overlapping id property with different types
type Problem = { id: string } & { id: number };
// Results in { id: never } — impossible to satisfy

When TypeScript shows you a type that resolves to never in an intersection, it's almost always because of a conflicting property. Check your constituent types for shared property names and ensure they agree on the type.

Key Takeaways

  • Intersection types use & to combine multiple types into one — a value must satisfy all members simultaneously
  • The resulting type has every property from every constituent type
  • Conflicting properties with incompatible types produce never, making the type unsatisfiable
  • Use extends for interface inheritance hierarchies; use & for composing type aliases and augmenting external types
  • Intersection types enable mixin-style composition — combining independent capability types without class inheritance

You now understand both sides of TypeScript's type composition toolkit: union types for OR logic and intersection types for AND logic. These two primitives — | and & — underpin nearly every advanced type pattern you'll encounter. Next: literal types, which take union types to the next level by constraining values to exact possibilities.