Right, so you’ve got your interfaces and your type aliases. You’re feeling good. You can describe the shape of your data. But then you realize the world isn’t static. A User object suddenly needs a isAdmin flag. A Car needs to become a FlyingCar. You need to compose types, not just declare them. This is where we move from simply describing data to architecting it.

The two tools in our toolbox for this are extends for interfaces and & (intersection) for type aliases. They are conceptually similar but, in classic TypeScript fashion, they have just enough differences to keep you on your toes.

Extending an Interface

Think of interface extension like classical inheritance, but for shape. You’re saying, “This new interface has everything that old one has, plus these extra bits.” It’s declarative, clean, and very readable.

// Your basic user. A classic.
interface User {
  name: string;
  email: string;
}

// Suddenly, we need privileged users. An admin is a User, but with more power.
interface Admin extends User {
  permissionLevel: 'moderator' | 'super-admin';
  banUser: (userId: string) => void;
}

// This works perfectly. The `Admin` type requires all of `User`'s properties plus its own.
const myAdmin: Admin = {
  name: 'Alice',
  email: 'alice@company.com',
  permissionLevel: 'super-admin',
  banUser: (id) => { console.log(`Banned user ${id}!`); }
};

You can also extend multiple interfaces. This is incredibly useful for mixing in properties from several sources, which is a pattern you see all the time in real-world applications.

interface CanDebug {
  debug: () => void;
}

interface CanLog {
  writeToLog: (message: string) => void;
}

// The ultimate developer: part User, part Debugger, part Logger.
interface Developer extends User, CanDebug, CanLog {
  favoriteLanguage: string;
}

const me: Developer = {
  name: 'You',
  email: 'you@awesome.dev',
  favoriteLanguage: 'TypeScript', // obviously
  debug: () => { console.log('Debugging...'); },
  writeToLog: (msg) => { console.log(`LOG: ${msg}`); }
};

The beauty here is in the error messages. If you miss a property, TypeScript will very clearly tell you which specific interface it was expecting it from.

Intersecting Type Aliases

Now, for type aliases, we don’t use extends. We use the & (intersection) operator. The name says it all: you’re telling the type system, “This new type must satisfy every one of these types simultaneously.” It’s a logical AND operation for types.

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

type Employee = {
  employeeId: number;
  startDate: Date;
};

// A UserWhoIsAlsoAnEmployee must have all properties from both User and Employee.
type UserWhoIsAlsoAnEmployee = User & Employee;

const newHire: UserWhoIsAlsoAnEmployee = {
  name: 'Bob',
  email: 'bob@company.com',
  employeeId: 12345,
  startDate: new Date() // You forgot this one? Yeah, TS will yell. Helpfully.
};

So far, it looks identical to extends, right? Here’s where the path gets rocky. The main difference comes when you try to combine types that have property conflicts.

The Key Difference: Handling Conflicts

This is the part the cheerful tutorials often gloss over. What happens if you intersect two types that have the same property, but with different, incompatible types?

An interface will loudly complain at the point of extension that you’re creating an impossible type.

interface Box {
  contents: string;
}

// ERROR: Interface 'ConflictingBox' cannot simultaneously extend types 'Box' and 'OtherBox'.
interface ConflictingBox extends Box, OtherBox {
}
interface OtherBox {
  contents: number; // <-- Conflict! Is it a string or a number?
}

The intersection type, however, will seemingly allow you to create the type alias. It’s only when you try to assign a value to that type that you realize you’ve created a logical monster.

type Box = { contents: string };
type OtherBox = { contents: number };

// No error here... the horror is lurking.
type ImpossibleBox = Box & OtherBox;

// Now we try to create one. Try it. I dare you.
const myBox: ImpossibleBox = {
  // The only valid value for `contents` would be something that is BOTH a string AND a number.
  // There is no such value in the universe. This type is effectively `never`.
  contents: "hello" // Error: Type 'string' is not assignable to type 'never'.
};

The type ImpossibleBox resolves to { contents: never } because string & number is never. This is a common pitfall. Intersection seems more flexible, but that flexibility lets you create nonsensical types that interfaces would have caught immediately. The lesson? Interfaces are stricter at the declaration site, while intersections are stricter at the usage site.

So, Which One Do I Use?

  • Use Interface Extension (extends) when you are building up hierarchies of objects in an object-oriented way. It’s the most intuitive choice for class-like structures and its error reporting is fantastic. The implements keyword also plays nicely with it.
  • Use Type Intersection (&) when you are composing more ad-hoc, functional, or generic types, especially union-based types (type A = B | C). It’s also your only choice for intersecting types that weren’t originally defined as interfaces.

My rule of thumb? If it’s a public API or something that represents a concrete object (like a User, ApiResponse, or Config), I almost always use an interface and extends. If it’s a helper type, a complex transformation, or something I’m manipulating within a utility type, I use type aliases and &. But honestly, on a good day, you can use either and be just fine. Just know what happens when you step on a rake.