TypeScriptIntersection Types

Intersection Types

Intersection types let you combine multiple types into one. The resulting type has all the properties from every constituent type. You create them with the & operator.

Think of it this way: if a union type (A | B) means "either A or B", an intersection type (A & B) means "both A and B at the same time".

The & Operator

The simplest intersection combines two object types. The resulting type requires every property from both sides.

TS
type Person = {
  name: string;
  age: number;
};

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

// StaffMember requires ALL four properties
type StaffMember = Person & Employee;

const alice: StaffMember = {
  name: 'Alice',
  age: 30,
  employeeId: 'E-1042',
  department: 'Engineering',
};
Note
An intersection type is purely a type-level operation — it has zero runtime cost. No merging happens at runtime; TypeScript simply checks that both shapes are satisfied.
Mental Model: Intersection vs Union

The naming can feel backwards at first. Here is a way to remember it:

  • Union (|) — the set of valid values is wider. A value only needs to satisfy one side.
  • Intersection (&) — the set of valid values is narrower. A value must satisfy all sides simultaneously.

The type gains more requirements, so fewer values qualify.

Concept

Union A | B

Intersection A & B

Required properties

Properties on A OR on B

Properties on A AND on B

Value set

Wider (more values qualify)

Narrower (fewer values qualify)

Analogy

Can speak English OR Spanish

Must speak English AND Spanish

Combining Object Types (Mixins)

A common pattern is building richer types from small, focused building-block types — sometimes called mixins or traits. Each block describes one concern; intersect them to get the full shape.

TS
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type SoftDeletable = {
  deletedAt: Date | null;
};

type Auditable = {
  createdBy: string;
  updatedBy: string;
};

// Combine all three concerns without repeating fields
type AuditedRecord = Timestamped & SoftDeletable & Auditable;

function saveRecord(record: AuditedRecord) {
  console.log(`Saved by ${record.createdBy} at ${record.createdAt}`);
}
Tip
Keep each building-block type small and focused on one concern. Intersections scale well when the individual pieces are cohesive — aim for types that do one thing well.
Adding Metadata to an Existing Type

A very practical use case is enriching a type you do not own (third-party or domain) with extra metadata, without modifying the original definition. This comes up constantly when wrapping API responses.

TS
// Third-party type you don't own
type Product = {
  id: string;
  name: string;
  price: number;
};

// Your internal UI metadata
type WithLoadingState = {
  isLoading: boolean;
  error: string | null;
};

type ProductViewModel = Product & WithLoadingState;

const viewModel: ProductViewModel = {
  id: 'p-99',
  name: 'Mechanical Keyboard',
  price: 149,
  isLoading: false,
  error: null,
};
Practical Example: Paginated API Response

APIs frequently return data alongside pagination metadata. Intersection types model this cleanly — define a reusable Paginated shape and intersect it with any concrete data type.

TS
type Paginated = {
  page: number;
  pageSize: number;
  totalCount: number;
  totalPages: number;
};

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

type UserListResponse = { users: User[] } & Paginated;

async function fetchUsers(page: number): Promise<UserListResponse> {
  const res = await fetch(`/api/users?page=${page}`);
  return res.json();
}

async function example() {
  const data = await fetchUsers(1);
  console.log(`Page ${data.page} of ${data.totalPages}`);
  data.users.forEach(u => console.log(u.name));
}
Success
This pattern works for any resource — replace User with Post, Order, or any other domain type. The Paginated definition lives in one place and is reused everywhere.
Intersection with Primitives

Intersecting two incompatible primitive types resolves to never — a type that has no possible values. TypeScript uses never as the "bottom type" (nothing can be assigned to it).

TS
type Impossible = string & number;
// => never

// Assigning to never always errors
const x: string & number = 'hello';
// Error: Type 'string' is not assignable to type 'never'

// Intersecting with 'unknown' is safe — result is the original type
type Same = string & unknown; // => string

// Intersecting with 'never' propagates never
type AlsoNever = string & never; // => never
Warning
If you see never appearing unexpectedly in your types, look for an intersection with conflicting primitive types or an impossible constraint somewhere in the chain.
Property Conflicts

When two intersected object types share a property name with incompatible types, the shared property becomes never. TypeScript does not error at the intersection definition itself — the error surfaces only when you try to assign a concrete value.

TS
type A = { id: string };
type B = { id: number };

type Conflict = A & B;
// Conflict.id is: string & number => never

const obj: Conflict = {
  id: 'hello', // Error: Type 'string' is not assignable to type 'never'
};

// Safe pattern: compatible shared properties narrow correctly
type C = { id: string; role: 'admin' };
type D = { id: string; role: 'admin' | 'user' };

type Merged = C & D;
// Merged.id   => string
// Merged.role => 'admin' & ('admin' | 'user') => 'admin'  ✓
Note
When two types share a property, the intersection narrows that property to the overlap of both types. 'admin' & ('admin' | 'user') simplifies to 'admin' because only 'admin' satisfies both constraints.
Nested Intersections

Intersections are associative — A & B & C is the same regardless of grouping. TypeScript flattens them. You can also build intersections incrementally with named intermediate types.

TS
type WithId    = { id: string };
type WithName  = { name: string };
type WithEmail = { email: string };

// All three are equivalent
type ContactA = WithId & WithName & WithEmail;
type ContactB = (WithId & WithName) & WithEmail;
type ContactC = WithId & (WithName & WithEmail);

// Build incrementally with readable intermediate types
type Named   = WithId & WithName;
type Contact = Named & WithEmail; // same final shape
Intersection vs extends

Both interface extends and & combine shapes, but they differ in conflict handling and ergonomics.

Aspect

interface extends

Intersection &

Conflict handling

Compile error on conflicting property types

Silently produces never on that property

Works with

Interfaces and classes only

Any type — objects, unions, primitives

Declaration merging

Yes (interfaces can be re-opened)

No

Best for

OOP hierarchies, public APIs

Composing utility shapes, adding metadata

TS
// Using extends — error is immediate and clear
interface Base { id: string }
interface Child extends Base { id: number }
// Error: Property 'id' in type 'Child' is not assignable to the same
// property in base type 'Base'.

// Using & — conflict is silent until you try to use the type
type BaseT  = { id: string };
type ChildT = BaseT & { id: number }; // No error here...
const c: ChildT = { id: 'x' };       // ...error surfaces here
Tip
Prefer interface extends when modelling class hierarchies or public library types where early conflict detection matters. Reach for & when composing utility shapes or adding metadata to types you do not own.
Function Overloads via Intersection

You can model a function that accepts multiple call signatures using an intersection of function types. This is exactly how TypeScript describes many built-in overloaded functions.

TS
// A function callable in two different ways
type Parser = {
  (input: string): number;
  (input: number): string;
};

const parse: Parser = (input: any): any => {
  if (typeof input === 'string') return parseFloat(input);
  return String(input);
};

const num = parse('3.14'); // => number
const str = parse(42);     // => string
Common Mistakes
  1. Expecting intersection to merge conflicting property types gracefully — it produces never instead.

  2. Confusing & with | (intersection narrows, union widens).

  3. Intersecting an object type with a primitive hoping to "brand" it — use a proper branded-type pattern with a unique symbol instead.

  4. Deeply nesting intersections when a single flat type alias with all properties would be cleaner.

Quick Reference
  • A & B — a value must satisfy both A and B simultaneously

  • Shared properties are narrowed to the overlap of both types

  • Incompatible shared properties collapse to never

  • Primitive intersections with no overlap produce never

  • Great for: adding metadata, composing mixin types, paginated responses

  • Prefer extends for class hierarchies; prefer & for composing utility shapes