Alright, let’s talk about intersection types. You’ve just seen how union types (A | B) are about giving you options. Intersection types (A & B), on the other hand, are about forcing you to have everything. It’s the type system’s way of saying, “You must be this tall and have this mustache to ride this ride.”

Think of an intersection type as a mash-up. If you have type A & B, you’re creating a new type that must have all the properties of A and all the properties of B. It’s composition. It’s telling you that for an object to be valid here, it needs to satisfy the contract of A and the contract of B simultaneously.

The “Merging” of Object Types

This is the most straightforward and common use. You’re stitching together multiple object types to create a new, richer type.

interface Loggable {
  log: () => void;
}

interface Serializable {
  serialize: () => string;
}

// This function expects an argument that is BOTH Loggable AND Serializable.
function persistAndLog(item: Loggable & Serializable) {
  item.log();
  saveToDisk(item.serialize());
}

// Our object must fulfill both contracts.
const myObject = {
  log: () => console.log('Logging...'),
  serialize: () => '{"data": "serialized"}'
};

persistAndLog(myObject); // Works perfectly.

Here, persistAndLog doesn’t care if you pass it one object or two objects stapled together. It only cares that the single thing you give it has both a .log() and a .serialize() method. This is incredibly useful for creating functions that require a set of capabilities from their arguments without needing to create a monolithic interface that describes every possible combination.

The Intersection Gotcha: Primitive Types

Now, let’s address the absurd part. What happens if you try to intersect two primitive types? Buckle up.

type NumberAndString = number & string;
// What is `NumberAndString`?

What value could possibly be both a number and a string at the same time? The only logical answer is… nothing. There is no such value. TypeScript represents this logical impossibility as the type never. It’s the type system’s way of saying “this can never happen.” You’ll mostly encounter this by accident when your type operations go sideways, not because you explicitly write number & string.

How Intersection Really Works (It’s Not Magic)

It’s crucial to understand that & doesn’t merge at runtime; it’s a compile-time constraint. The underlying JavaScript is still just a duck-typed object. TypeScript’s job is to ensure that object has the right shape. The intersection operator is just a way to describe that required shape more precisely.

This becomes especially important when intersecting incompatible object types.

interface Foo {
  prop: number;
  conflicting: string;
}

interface Bar {
  otherProp: boolean;
  conflicting: number; // Same property name, different type!
}

type Problematic = Foo & Bar;

// Let's try to create one... what should `conflicting` be?
const badExample: Problematic = {
  prop: 42,
  otherProp: true,
  conflicting: 'oops' // Type 'string' is not assignable to type 'never'.
};

See that? The property conflicting now has the type string & number. And as we just learned, that resolves to never. You can’t assign any value to it. The designers gave us a powerful tool, but this is the clear questionable choice: instead of warning you that you’re creating an impossible type, it quietly creates a property that can never be satisfied. You must be vigilant about property name clashes when using intersections.

Best Practices and the “Mixins” Pattern

The real power of intersection types shines when composing types without inheritance. The “mixins” pattern is a classic example. Instead of deep inheritance hierarchies (Employee -> Person -> Mammal -> LivingOrganism), you can compose capabilities.

// Some "mixin" interfaces
interface CanSpeak {
  speak: () => void;
}
interface CanCode {
  writeCode: () => void;
}
interface CanManage {
  scheduleMeeting: () => void;
}

// Compose a role on the fly
type Developer = CanSpeak & CanCode;
type TechLead = CanSpeak & CanCode & CanManage;

function hireDeveloper(dev: Developer) {
  dev.speak();
  dev.writeCode();
}

const myNewHire: TechLead = {
  speak: () => console.log("Hello world!"),
  writeCode: () => console.log("Writing code..."),
  scheduleMeeting: () => console.log("Let's sync.")
};

hireDeveloper(myNewHire); // This is perfectly valid. A TechLead IS a Developer.

This is far more flexible. You can describe exactly what a function needs without forcing the argument to inherit from a specific base class. It’s the ultimate “duck typing” enabler. If it quacks and writes code, it’s a Developer as far as my function is concerned.

In summary, use intersection types to demand multiple capabilities from a single object. It’s your go-to tool for composition over inheritance. But always, always, keep an eye out for those property name clashes—they’ll sneak up and bite you with a never when you least expect it.