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.
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',
};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.
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}`);
}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.
// 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.
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));
}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).
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
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.
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' ✓'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.
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 shapeIntersection 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 |
// 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 hereinterface 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.
// 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); // => stringCommon Mistakes
Expecting intersection to merge conflicting property types gracefully — it produces never instead.
Confusing & with | (intersection narrows, union widens).
Intersecting an object type with a primitive hoping to "brand" it — use a proper branded-type pattern with a unique symbol instead.
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 simultaneouslyShared properties are narrowed to the overlap of both types
Incompatible shared properties collapse to
neverPrimitive intersections with no overlap produce
neverGreat for: adding metadata, composing mixin types, paginated responses
Prefer
extendsfor class hierarchies; prefer&for composing utility shapes