TypeScript Extending Interfaces

Interfaces in TypeScript are not just standalone shapes — they can build on each other. The extends keyword lets one interface inherit all the members of another, so you can describe a hierarchy of related types without repeating yourself. This is how you model relationships like “a Dog is an Animal, plus some extra stuff” at the type level, with zero runtime cost.

Overview / How Interface Extension Works

When an interface B extends an interface A, every property and method declared on A becomes part of B automatically. Any object that satisfies B must therefore satisfy A too — B is a more specific (narrower) version of A. This mirrors class inheritance conceptually, but interfaces have no implementation and no runtime existence at all: they exist purely to describe shapes, and the compiler erases them completely during compilation.

Because TypeScript uses structural typing, extending an interface is really just a convenient way of saying “merge these members together, and require the result to be internally consistent.” The compiler literally computes the union of members from the parent and child declarations and checks that any property appearing in more than one parent has compatible types everywhere it appears. If a child interface redeclares a property inherited from a parent, the child’s type for that property must be assignable to the parent’s type (you can narrow, but you cannot make it incompatible).

An interface can extend more than one interface at once, separated by commas. It can also extend a type alias that describes an object type, and — less commonly — it can even extend a class, in which case it inherits the class’s instance shape (including private/protected members, which can then only be satisfied by subclasses of that class). Extension is different from declaration merging, where you declare an interface with the same name twice and TypeScript automatically combines both declarations into one; extending is explicit and directional, merging is implicit and same-named.

Syntax

interface ChildInterface extends ParentInterface {
  additionalProperty: SomeType;
}

// Extending multiple interfaces
interface Combined extends InterfaceOne, InterfaceTwo {
  extraProperty: SomeType;
}
Part Meaning
ChildInterface The new interface being declared
extends Keyword that inherits members from one or more interfaces
ParentInterface The interface (or interfaces, comma-separated) whose members are inherited
additionalProperty A new member added on top of everything inherited

Examples

Example 1: Basic single extension

interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

const rex: Dog = {
  name: "Rex",
  age: 3,
  breed: "Labrador",
  bark() {
    console.log(`${this.name} says woof!`);
  },
};

rex.bark();
console.log(`${rex.name} is a ${rex.age}-year-old ${rex.breed}`);

Output:

Rex says woof!
Rex is a 3-year-old Labrador

The Dog interface inherits name and age from Animal and adds breed and bark(). Any value typed as Dog must supply all four members; the compiler treats the inherited ones exactly as if they had been written directly inside Dog.

Example 2: Extending multiple interfaces

interface Serializable {
  serialize(): string;
}

interface Timestamped {
  createdAt: Date;
}

interface LogEntry extends Serializable, Timestamped {
  message: string;
  level: "info" | "warn" | "error";
}

const entry: LogEntry = {
  message: "Server started",
  level: "info",
  createdAt: new Date("2024-01-01T00:00:00Z"),
  serialize() {
    return `[${this.level.toUpperCase()}] ${this.createdAt.toISOString()} - ${this.message}`;
  },
};

console.log(entry.serialize());

Output:

[INFO] 2024-01-01T00:00:00.000Z - Server started

LogEntry pulls members from two unrelated interfaces at once. This is a common pattern for composing small, reusable “capability” interfaces (like Serializable or Timestamped) into a single concrete shape, instead of writing one giant interface.

Example 3: Narrowing an inherited property

interface Shape {
  kind: string;
  color: string;
}

interface Circle extends Shape {
  kind: "circle";
  radius: number;
}

function area(circle: Circle): number {
  return Math.PI * circle.radius ** 2;
}

const c: Circle = { kind: "circle", color: "red", radius: 2 };
console.log(area(c).toFixed(2));

Output:

12.57

Circle redeclares kind, but narrows it from the general string in Shape down to the literal type "circle". This is legal because every value of type "circle" is also a valid string — the child’s type is assignable to the parent’s type. This “discriminant narrowing” pattern is exactly how discriminated unions of shapes (Circle | Square | Triangle) are usually built.

Under the Hood

When the compiler encounters interface B extends A { ... }, it performs roughly these steps:

  • It resolves all members of A (which may itself extend other interfaces, recursively).
  • It combines those members with the members declared directly in B.
  • For every member name that appears in both, it checks that B‘s type for that member is assignable to A‘s type. If it isn’t, you get a compile-time error — extension fails at the type level, not at runtime.
  • The resulting merged shape is what gets used everywhere B is referenced — there is no separate “parent object” or prototype chain involved, unlike class inheritance.

Crucially, none of this exists once compilation finishes. TypeScript’s type system, including every interface and extends clause, is completely erased at runtime. The compiled JavaScript for the examples above contains only the object literals and function calls — there is no Animal, Dog, or Circle anywhere in the output .js file. Interface extension is a compile-time-only contract that helps you and the compiler reason about shapes; it has zero footprint and zero performance cost in the running program.

Common Mistakes

Mistake 1: Overriding a property with an incompatible type

interface Base {
  id: string;
}

interface Derived extends Base {
  id: number;
}

This fails to compile with an error similar to: Interface 'Derived' incorrectly extends interface 'Base'. Types of property 'id' are incompatible. A child interface can narrow an inherited property’s type, but it cannot change it into something the parent’s type isn’t compatible with — number is not assignable to string, so this combination is rejected outright.

The fix is to either keep the same type or narrow it to a compatible subtype (like a specific string literal), not switch to a completely different type:

interface Base {
  id: string;
}

interface Derived extends Base {
  id: string;
  extra: boolean;
}

const item: Derived = { id: "abc123", extra: true };
console.log(item.id, item.extra);

Output:

abc123 true

Mistake 2: Extending two interfaces with a conflicting property

interface A {
  value: string;
}

interface B {
  value: number;
}

interface C extends A, B {
  extra: boolean;
}

This produces: Interface 'C' cannot simultaneously extend types 'A' and 'B'. Named property 'value' of types 'A' and 'B' are not identical. When extending multiple interfaces, every property they have in common must have the exact same type in each — TypeScript won’t try to reconcile string and number for you.

The usual fix is to rename the conflicting property in one of the source interfaces, or restructure so the two interfaces don’t overlap on incompatible types:

interface A {
  value: string;
}

interface B {
  count: number;
}

interface C extends A, B {
  extra: boolean;
}

const item: C = { value: "hello", count: 5, extra: true };
console.log(item.value, item.count, item.extra);

Output:

hello 5 true

Best Practices

  • Keep parent interfaces small and focused (one capability each, like Serializable or Timestamped) so they can be mixed into many child interfaces via multiple extends.
  • Prefer narrowing a property’s type in a child interface (e.g. string"circle") over trying to change it to an unrelated type — the compiler will reject the latter anyway.
  • When extending multiple interfaces, watch for overlapping property names; give shared concepts identical types across all parents to avoid conflicts.
  • Use extends for “is-a-more-specific-version-of” relationships; if you’re just gluing unrelated shapes together for a single use, an intersection type (type X = A & B) may read more clearly.
  • Remember interfaces (and their extends chains) vanish at compile time — they cost nothing at runtime, so don’t hesitate to build deep, descriptive hierarchies where they genuinely model your domain.
  • Don’t confuse extending (explicit, via extends) with declaration merging (implicit, via two same-named interface declarations) — they solve different problems and mixing them up leads to confusing type shapes.

Practice Exercises

  • Define a Vehicle interface with make: string and year: number. Then define a Car interface that extends it, adding doors: number and a method honk(): void. Create a Car object and call honk().
  • Create two interfaces, HasId (with id: string) and HasName (with name: string), then define a User interface that extends both and adds an email: string property. Build a sample User object and log it with console.log.
  • Take the Shape/Circle example from this lesson and add a second interface Square extends Shape with kind: "square" and side: number. Write a function that accepts Circle | Square and returns the area for either, using the kind property to distinguish them.

Summary

  • extends lets one interface inherit all members of one or more other interfaces.
  • A child interface can narrow an inherited property’s type but cannot make it incompatible with the parent’s type.
  • Extending multiple interfaces requires that any shared property names have identical types across all of them.
  • Interface extension is purely a compile-time construct based on structural typing — it is completely erased at runtime, with no cost to the compiled JavaScript.
  • Use small, focused parent interfaces and combine them via multiple extends to build precise, descriptive object shapes.