Search⌘ K
AI Features

Interfaces and Extensions

Explore the concepts of interfaces and extensions in TypeScript to define reusable object contracts and create scalable type hierarchies. This lesson helps you understand when to choose interfaces over type aliases and shows how to extend interfaces for evolving codebases. By mastering these patterns, you'll enhance your ability to write clear, maintainable, and adaptable TypeScript code suitable for real-world applications.

If you’ve been using type to model object shapes, that’s totally valid. But the interface is often a better choice—one that’s built for scalecollaboration, and evolving codebases.

Interfaces aren’t just a different way to describe object structure. They’re how we model contracts. When we define an interface, we’re saying: “This shape matters. It’s meant to be reused. It’s meant to grow.”

In this lesson, we’ll learn how to declare interfaces, extend them cleanly, and understand exactly how they compare to type aliases. By the end, we’ll know when and why to reach for each and how to build strong, adaptable types that play well with real-world systems.

Declaring interfaces

Interfaces let us define the shape of an object—its required properties and their types. Just like type aliases, they support optional fields (?) and readonly modifiers. And yes, we can use them anywhere we’d use a type: to annotate variables, parameters, return values, and more.

interface User {
readonly id: number;
username: string;
isAdmin: boolean;
email?: string;
}
Declaring an interface to describe the shape of a User object

Explanation:

  • Line 1: The User interface begins with the interface keyword, introducing a reusable named shape.

  • Line 2: The id property is marked as readonly, meaning it can be set when the object is created but cannot be reassigned later.

  • Line 3: username is a required string—this must be present in any object that implements User.

  • Line 4: isAdmin is also required and must be a boolean.

  • Line 5: email is optional, as indicated by the ?. Objects may include it or leave it out.

If this feels familiar—like something we could do with a type alias—you’re not wrong. We’ll dig into when and why to choose one over the other later in this lesson. For now, just know this: interfaces are designed for contracts—structures you can extend, share, and evolve confidently across systems and teams.

Interfaces give our code clarity and predictability—especially when working in teams or across module boundaries.

Using interfaces in code

Every interface can be treated as a type so we can use it interchangeably in places where a type is expected—parameters, return values, variables, and more.

TypeScript 5.8.3
interface User {
readonly id: number;
username: string;
isAdmin: boolean;
email?: string;
}
function promoteUser(user: User): void {
if (!user.isAdmin) {
user.isAdmin = true;
}
}
const bob: User = {
id: 2,
username: "BobTheDev",
isAdmin: false,
};
promoteUser(bob);
console.log(bob);

Explanation:

  • Lines 1–6: We define a User interface with the required structure.

  • Line 8: The function promoteUser declares that its parameter must match the User interface.

  • Lines 9–11: The function checks isAdmin and updates it—TypeScript ensures the structure and property types are valid.

  • Lines 14–22: We create a User object and pass it into the function. The final log shows the updated isAdmin status.

Extending interfaces

Here’s where interfaces really start to differentiate themselves. In any serious application especially one built by multiple developers over time, we need to model things that evolve. We don’t just define shapes once and walk away. We build on them, specialize them, and extend them.

Interfaces are built for this.

With extends, we can layer on new fields, create specific versions of a generic shape, and compose clean, predictable type hierarchies. This is how we keep types DRYDRY stands for Don’t Repeat Yourself—a principle that encourages reusing logic instead of duplicating it., expressive, and scalable even as our app grows in complexity.

Let’s look at an example.

interface BaseResponse {
status: number;
message: string;
}
interface UserResponse extends BaseResponse {
user: User;
}
Extending a base interface to add specialized properties

Explanation:

  • Lines 1–4: BaseResponse defines a structure we might see from any API—fields like status and message that every response should include.

  • Line 6: UserResponse uses extends to build on the shared structure. This means it includes everything from BaseResponse—it can’t leave out status or message. Those fields are now part of the shape, just like user.

  • Line 7: It adds a user field, making this a specialized response shape for user-related data.

This kind of composition is what makes interfaces powerful at scale—they let us evolve type systems without rewriting or duplicating code.

Here’s a full example—combining shared and specific fields in a real response object and seeing how TypeScript enforces the contract every step of the way.

TypeScript 5.8.3
interface User {
readonly id: number;
username: string;
isAdmin: boolean;
email?: string;
}
interface BaseResponse {
status: number;
message: string;
}
interface UserResponse extends BaseResponse {
user: User;
}
function handleResponse(response: UserResponse): void {
console.log(`Status: ${response.status} - ${response.message}`);
console.log(`User: ${response.user.username} (${response.user.isAdmin ? "Admin" : "Standard User"})`);
}
const response: UserResponse = {
status: 200,
message: "User fetched successfully.",
user: {
id: 42,
username: "devLegend",
isAdmin: true,
},
};
handleResponse(response);

Explanation:

  • Lines 1–15: This setup defines a base API response, a user shape, and a specialized UserResponse that combines both through extension.

  • Lines 17–20: The handleResponse function expects a UserResponse—which includes both the base and specific fields.

  • Lines 22–32: We construct a valid UserResponse object and invoke it. If we omit status, message, or user, TypeScript will flag it immediately. Feel free to experiment by tweaking the object to see what the compiler catches.

Extending multiple interfaces (and even types)

We’ve seen how an interface can extend a single base shape but it doesn’t stop there.

Interfaces can also:

  • Extend multiple interfaces at once, combining all their fields.

  • Extend types as long as those types resolve to object shapes (not unions or primitives).

This gives us a powerful way to compose behavior from multiple sources cleanly and safely.

TypeScript 5.8.3
interface Identifiable {
id: number;
}
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
interface AuditedEntity extends Identifiable, Timestamped {
modifiedBy: string;
}
const record: AuditedEntity = {
id: 101,
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-04-01"),
modifiedBy: "system",
};
console.log(record);

Explanation:

  • Line 10: We declare an interface AuditedEntity that extends both the Identifiable interface and the Timestamped type alias. This means:

    • TypeScript merges the members from both Identifiable and Timestamped.

    • Then, it adds modifiedBy: string as a new property.

  • Lines 14–19: We create a record object that satisfies the AuditedEntity interface. It includes all required properties: id, createdAt, updatedAt, and modifiedBy.

We’ll see this pattern often in real-world APIs, domain models, and libraries especially in systems that evolve or layer behavior over time.

Note: An interface can extend any identifier that ultimately resolves to an object type. However, it cannot extend a union type or a primitive. Extending an intersection of object typesIt is a way to combine multiple types into one by requiring all properties from each (we'll learn about intersections in more detail soon). is perfectly valid because the resulting shape is still an object.

Declaration merging: A superpower

One of the most unique features of interface is that it can be declared multiple times and TypeScript will automatically merge them. This is incredibly useful in larger projects, where types are extended across files, layers, or even third-party packages.

Instead of redefining or duplicating structures, we can expand them piece by piece. Let’s see it in action:

TypeScript 5.8.3
interface Settings {
darkMode: boolean;
}
interface Settings {
autoSave: boolean;
}
const config: Settings = {
darkMode: true,
autoSave: false,
};
console.log(config);

Explanation:

  • Lines 1 and 5: We declare Settings twice. TypeScript merges both declarations into one unified shape.

  • Lines 9–12: config satisfies the merged structure—both darkMode and autoSave are present.

Interface vs. type: When to choose which

At first glance, interface and type might seem interchangeable. Both can describe object shapes, and in many cases, either will work. But under the hood, they behave differently and those differences start to matter as our codebase grows or our types get more complex.

Here’s a side-by-side breakdown of how they compare:

Capability/Use Case

interface

type

Clarification

Describe an object shape.

Yes

Yes

Both define object structures, including nested and callable shapes.

Extend another shape using extends.

Yes

No

Only interface supports extends; type must use & to combine.

Combine using

& (intersection).

No

Yes

Interfaces can’t use & directly, but they can be used inside a type alias.

Combine using

| (union types).

No

Yes

Interfaces can’t use | directly, but they can be used inside a type alias.

Merge declarations with same name.

Yes

No

Interfaces are “open”—multiple declarations are merged. Types are “closed.”

Describe primitives like

string.

No

Yes

Only type aliases can directly represent primitives or literal types.

Use with classes (implements).

Yes

Yes (if object-shaped)

A class can implement either, but a type must resolve to an object shape.

Note: No need to worry about intersection types (&) or about classes—we'll cover both later in the course.

General rule of thumb:

  • Use interface when you’re designing object shapes that may evolve, be extended, or shared across your codebase.

  • Use type when you need flexibility like composing multiple types, modeling unions, or working with non-object structures.

Key takeaways:

  • Interfaces define clear, reusable contracts for object structures and can also be used to type function parameters and class implementations.

  • Interfaces can be extended using extends, and even support multiple base interfaces or object-like type aliases.

  • Interface declarations are “open” and can merge across files, making them ideal for modular codebases and third-party extension.

  • Every interface can be treated as a type, but not every type can behave like an interface.

  • Prefer interface when designing shapes that may grow, evolve, or be shared especially across APIs, libraries, or large teams.

  • Use type when you need to model non-object structures, create unions, or compose types more freely.

Exercise: Logging electric vehicle information

You’re building a system to manage vehicle data.

  1. Create a base interface called Vehicle with the properties: make, model, and year.

  2. Then, create an interface called ElectricVehicle that extends Vehicle and adds a batteryCapacity field.

  3. Finally, implement a function called logEVInfo that accepts an ElectricVehicle and logs a string like "2024 Tesla Model 3 – 75 kWh battery". Let us know if you want the solution section updated to match this formatting as well.

Good luck!

TypeScript 5.8.3
// Write your code here

Try solving the problem on your own first. If you’re feeling stuck, you can click “Show Hints” for guidance or “Show Solution” to view the complete answer.