The extends keyword creates a new interface that contains every property and method of an existing interface, plus the members we add to it. In TypeScript, an interface can extend one interface or several at once, and the compiler checks that an object of the new type has all inherited members as well as its own. We use it to build related types, such as User and Admin, without repeating the shared properties.
Java developers know the syntax, because interface Admin extends User means the same thing in both languages, and in both an interface may extend several parents. TypeScript adds two features that Java does not have.
- Two interface declarations with the same name merge into one (declaration merging).
- The & operator combines types without declaring a new interface at all (an intersection type).
The following example shows all four forms in one block, compiled with TypeScript 7.0.2 and run on Node.js 22. Each form needs only a few lines.
interface User {
name: string;
age: number;
}
interface Printable {
print(): string;
}
// 1. Extend one interface
interface Admin extends User {
role: string;
}
const lokesh: Admin = { name: "Lokesh", age: 37, role: "owner" };
// 2. Extend several interfaces
interface PrintableUser extends User, Printable {}
const raj: PrintableUser = {
name: "Raj",
age: 35,
print() { return this.name + " (" + this.age + ")"; },
};
const text = raj.print(); // text = "Raj (35)"
// 3. Merge two declarations with the same name
interface Settings { theme: string; }
interface Settings { fontSize: number; }
const settings: Settings = { theme: "dark", fontSize: 14 };
// 4. Combine types with an intersection
type AdminAndPrintable = Admin & Printable;
const john: AdminAndPrintable = {
name: "John",
age: 40,
role: "editor",
print() { return this.name + " is " + this.role; },
};
const line = john.print(); // line = "John is editor"
Notice that Settings has no extends at all, because the two declarations with the same name merge into one interface.
Next, we extend one and several interfaces and redeclare an inherited property with a narrower type. After that, we replace properties with Omit and Partial, extend generic interfaces and classes, and compare declaration merging and intersection types with extends, including what happens when properties conflict.
1. Extending an Interface With extends
The interface after extends is the base interface, and the new one is the derived interface, which gets all members of the base and lists only what is new. In the snippet, Square inherits name and area() from Shape and adds side. For example, an online shop can have a Product interface and a DigitalProduct interface that extends it with a download link.
// 1. Base interface
interface Shape {
name: string;
area(): number;
}
// 2. Derived interface: inherits name and area(), adds side
interface Square extends Shape {
side: number;
}
const square: Square = {
name: "square",
side: 4,
area() { return this.side * this.side; },
};
const area = square.area(); // area = 16
// 3. A Square can be used wherever a Shape is expected
const shape: Shape = square;
const shapeName = shape.name; // shapeName = "square"
An object of type Square must provide all three members, so leaving out name gives the same “Property ‘name’ is missing” error as for a plain interface. The last two lines show the direction of assignment. A Square is also a Shape, so it can be passed to any function that accepts a Shape. The opposite is not true, because a plain Shape has no side.
Since TypeScript compares types by their structure, the assignment of a Square to a Shape would also work without extends, because a separately declared interface with the same three members is assignable to Shape as well. The extends clause saves us from repeating the members and keeps the two types in sync when Shape changes. Optional and readonly members of an interface are inherited in the same way.
2. Redeclaring an Inherited Property With a Narrower Type
A derived interface may declare an inherited property again, as long as the new type is assignable to the old one. In practice, we can make the type narrower, but not wider or different. Here kind is any string in Shape and only the literal “square” in Square.
interface Shape {
kind: string;
area(): number;
}
// kind is redeclared with a narrower type
interface Square extends Shape {
kind: "square";
side: number;
}
const square: Square = {
kind: "square",
side: 3,
area() { return this.side * this.side; },
};
const kind = square.kind; // kind = "square", type "square"
const area = square.area(); // area = 9
The rule keeps every Square usable as a Shape. If Square declared kind: number, code that reads shape.kind as a string would receive a number. The compiler rejects the kind: number declaration with error TS2430.
error TS2430: Interface 'Square' incorrectly extends interface 'Shape'.
Types of property 'kind' are incompatible.
Type 'number' is not assignable to type 'string'.
Narrowing a property to a literal type is common in discriminated unions, where a kind field tells the compiler which variant an object is. The value “square” is a literal type, and a Square combined with other shapes forms a union type that the compiler narrows by kind.
3. Extending Multiple Interfaces
An interface can list several base interfaces after extends, separated by commas, and the derived interface receives the members of all of them. Multiple inheritance of interfaces works the same way as in Java, and it lets us build a type from small, focused interfaces. For example, a blog app builds an Article type from small interfaces such as Timestamped and Publishable.
interface Named {
name: string;
}
interface Aged {
age: number;
}
interface Printable {
print(): string;
}
// One interface, three parents
interface User extends Named, Aged, Printable {
email?: string;
}
const lokesh: User = {
name: "Lokesh",
age: 37,
print() { return this.name + ", " + this.age; },
};
const printed = lokesh.print(); // printed = "Lokesh, 37"
When two base interfaces declare the same property, the types must be identical. For example, extending both User (with age: number) and a LegacyUser interface (with age: string) fails with error TS2320, because no value can be a number and a string at the same time.
error TS2320: Interface 'Mixed' cannot simultaneously extend types 'User' and 'LegacyUser'.
Named property 'age' of types 'User' and 'LegacyUser' are not identical.
The conflict has two fixes.
- Redeclare age in the derived interface with a type that fits both parents.
- Remove the conflicting property from one parent with Omit, as shown in section 4.
4. Changing Inherited Properties With Omit and Partial
Sometimes we want most of a base interface, but one property needs a different type. For example, an HTML form returns every field as text, so the form type needs age as a string. The narrowing rule from section 2 blocks a type such as string for a number property. The utility type Omit<User, “age”> builds a copy of User without age, and the derived interface declares age with the new type.
Partial<User> is another utility type that makes every property optional. Extending it is useful for update requests, where the caller sends only the fields that changed.
interface User {
name: string;
age: number;
}
// 1. Replace a property: age comes from a text field as a string
interface UserForm extends Omit<User, "age"> {
age: string;
}
const form: UserForm = { name: "Raj", age: "35" };
// 2. Make every inherited property optional
interface UserUpdate extends Partial<User> {
id: number;
}
const update: UserUpdate = { id: 1, age: 38 };
const formAge = Number(form.age); // formAge = 35
const newAge = update.age; // newAge = 38
const newName = update.name; // newName = undefined
An interface can extend these utility types because they produce object types with known members. Other utility types, such as Pick and Required, work with extends in the same way.
5. Extending Generic Interfaces and Classes
A generic interface is extended like any other, and the derived interface can pass its own type parameter to the base. For example, LabeledBox<T> keeps the value: T property from Box<T> and adds a label.
TypeScript also lets an interface extend a class, which Java does not allow. The interface takes the class’s instance members as declarations, without their code. Here ColoredRectangle gets width and height from the Rectangle class.
// 1. Extending a generic interface
interface Box<T> {
value: T;
}
interface LabeledBox<T> extends Box<T> {
label: string;
}
const ageBox: LabeledBox<number> = { label: "Lokesh", value: 37 };
// 2. An interface that extends a class
class Rectangle {
width = 0;
height = 0;
}
interface ColoredRectangle extends Rectangle {
color: string;
}
const red: ColoredRectangle = { width: 2, height: 3, color: "red" };
const area = red.width * red.height; // area = 6
There is one restriction. If the class has a private or protected member, the interface inherits that member too, and only the class or its subclasses can provide it. In that case, a plain object literal fails with “Property ‘balance’ is missing”, where balance is the private field. Interfaces that extend classes are rare in application code, whereas generic interfaces are far more common.
6. Declaration Merging
When we declare two interfaces with the same name in the same scope, TypeScript merges them into a single interface that has the members of both, without any keyword. The compiler treats the two User blocks as one interface with name and age.
// Two declarations, one interface
interface User {
name: string;
}
interface User {
age: number;
}
const john: User = { name: "John", age: 40 }; // both properties required
const keys = Object.keys(john); // keys = ["name", "age"]
If both declarations contain the same property, its type must be the same in both. Declaring age: number in one block and age: string in another gives error TS2717: Subsequent property declarations must have the same type.
In application code, we rarely write merging on purpose, so a second declaration in the same file is most often a mistake. Merging is for extending types we do not own. Library authors declare their types as interfaces, so our code can add a property to them, for example to the request type of a web framework or to a global type. The two forms used for that are module augmentation and global augmentation. A type alias cannot be merged, and a second type User declaration is a “Duplicate identifier” error.
7. Intersection Types With the & Operator
An intersection type, written A & B, describes a value that has all members of A and all members of B. It works with interfaces, type aliases and inline object types, and it does not need a new named declaration. We can even write it inline in a parameter type, as the describe() function shows.
interface Shape {
area(): number;
}
type Colored = {
color: string;
};
// 1. Intersection of an interface and a type alias
type ColoredShape = Shape & Colored;
const tile: ColoredShape = {
color: "blue",
area() { return 2 * 2; },
};
const area = tile.area(); // area = 4
// 2. Intersection written inline in a parameter
function describe(shape: Shape & Colored): string {
return shape.color + " " + shape.area();
}
const text = describe(tile); // text = "blue 4"
For types without conflicts, interface ColoredShape extends Shape, Colored {} and type ColoredShape = Shape & Colored accept the same objects. The difference shows when a property appears in both parts with different types. The extends clause reports the conflict at the declaration (TS2320, as in section 3). An intersection is accepted, but the conflicting property gets the type never, which is a type with no possible values.
type Strange = { age: number } & { age: string };
const s: Strange = { age: 37 };
error TS2322: Type 'number' is not assignable to type 'never'.
The error appears only when we try to create a value, far from the type that caused it. The late error is the main reason to prefer extends for object types that form a hierarchy.
| Question | interface extends | Intersection with & |
|---|---|---|
| Needs a named interface | Yes | No, works inline |
| Can combine union types | No, only object types | Yes |
| Conflicting property types | Error at the declaration | Property becomes never |
| Redeclare an inherited property | Yes, with a narrower type | Not possible; types are combined |
| Merges with a second declaration | Yes | No |
We use extends to build a hierarchy of named object types, and & to combine types inline, for example when one side is a union or a type alias.
8. Compiling the Code From This Article
The extend-interface-example folder on GitHub contains one source file per section and an index.ts that runs them. It needs Node.js 22 or newer and compiles with TypeScript 7.0.2.
npm install
npm start
The output prints each value from the snippet comments under a header for its section. The code that fails to compile is not in the project, so the build succeeds.
9. Conclusion
Extending an interface copies its members into a new interface, and a TypeScript interface can extend one or many interfaces, generic interfaces, utility types such as Omit and Partial, and even classes. An inherited property can only be redeclared with a narrower type. Declaration merging joins interfaces with the same name and exists mainly for adding to library types. Intersection types combine any types without a declaration, but they hide property conflicts as never instead of reporting them, so extends is the safer choice for object hierarchies.
10. References
These official pages describe interface inheritance and intersection types, plus the utility types and declaration merging from the examples.
- TypeScript Handbook: Extending Types
- TypeScript Handbook: Intersection Types
- TypeScript Handbook: Declaration Merging
- TypeScript Handbook: Utility Types
Happy Learning !!