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:
| Feature | extends | Intersection & |
|---|---|---|
| Works with | Interfaces and classes | Any type |
| Error messages | Clearer, points to the interface | Can be verbose |
| Conflicting properties | Compile error | Produces never |
| Multiple sources | extends A, B | A & 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
extendsisn'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
extendsinstead) - 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 satisfyWhen 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
extendsfor 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.