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 toA‘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
Bis 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
SerializableorTimestamped) so they can be mixed into many child interfaces via multipleextends. - 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
extendsfor “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
extendschains) 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
Vehicleinterface withmake: stringandyear: number. Then define aCarinterface that extends it, addingdoors: numberand a methodhonk(): void. Create aCarobject and callhonk(). - Create two interfaces,
HasId(withid: string) andHasName(withname: string), then define aUserinterface that extends both and adds anemail: stringproperty. Build a sampleUserobject and log it withconsole.log. - Take the
Shape/Circleexample from this lesson and add a second interfaceSquare extends Shapewithkind: "square"andside: number. Write a function that acceptsCircle | Squareand returns the area for either, using thekindproperty to distinguish them.
Summary
extendslets 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
extendsto build precise, descriptive object shapes.
