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 scale, collaboration, 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;}
Explanation:
Line 1: The
Userinterface begins with theinterfacekeyword, introducing a reusable named shape.Line 2: The
idproperty is marked asreadonly, meaning it can be set when the object is created but cannot be reassigned later.Line 3:
usernameis a required string—this must be present in any object that implementsUser.Line 4:
isAdminis also required and must be a boolean.Line 5:
emailis 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.
Explanation:
Lines 1–6: We define a
Userinterface with the required structure.Line 8: The function
promoteUserdeclares that its parameter must match theUserinterface.Lines 9–11: The function checks
isAdminand updates it—TypeScript ensures the structure and property types are valid.Lines 14–22: We create a
Userobject and pass it into the function. The final log shows the updatedisAdminstatus.
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
Let’s look at an example.
interface BaseResponse {status: number;message: string;}interface UserResponse extends BaseResponse {user: User;}
Explanation:
Lines 1–4:
BaseResponsedefines a structure we might see from any API—fields likestatusandmessagethat every response should include.Line 6:
UserResponseusesextendsto build on the shared structure. This means it includes everything fromBaseResponse—it can’t leave outstatusormessage. Those fields are now part of the shape, just likeuser.Line 7: It adds a
userfield, 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.
Explanation:
Lines 1–15: This setup defines a base API response, a
usershape, and a specializedUserResponsethat combines both through extension.Lines 17–20: The
handleResponsefunction expects aUserResponse—which includes both the base and specific fields.Lines 22–32: We construct a valid
UserResponseobject and invoke it. If we omitstatus,message, oruser, 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.
Explanation:
Line 10: We declare an interface
AuditedEntitythat extends both theIdentifiableinterface and theTimestampedtype alias. This means:TypeScript merges the members from both
IdentifiableandTimestamped.Then, it adds
modifiedBy: stringas a new property.
Lines 14–19: We create a
recordobject that satisfies theAuditedEntityinterface. It includes all required properties:id,createdAt,updatedAt, andmodifiedBy.
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
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:
Explanation:
Lines 1 and 5: We declare
Settingstwice. TypeScript merges both declarations into one unified shape.Lines 9–12:
configsatisfies the merged structure—bothdarkModeandautoSaveare 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 |
|
| Clarification |
Describe an object shape. | Yes | Yes | Both define object structures, including nested and callable shapes. |
Extend another shape using | Yes | No | Only interface supports |
Combine using
| No | Yes | Interfaces can’t use |
Combine using
| No | Yes | Interfaces can’t use |
Merge declarations with same name. | Yes | No | Interfaces are “open”—multiple declarations are merged. Types are “closed.” |
Describe primitives like
| No | Yes | Only type aliases can directly represent primitives or literal types. |
Use with classes ( | 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
interfacewhen you’re designing object shapes that may evolve, be extended, or shared across your codebase.Use
typewhen 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
interfacecan be treated as atype, but not everytypecan behave like aninterface.Prefer
interfacewhen designing shapes that may grow, evolve, or be shared especially across APIs, libraries, or large teams.Use
typewhen 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.
Create a base interface called
Vehiclewith the properties:make,model, andyear.Then, create an interface called
ElectricVehiclethat extendsVehicleand adds abatteryCapacityfield.Finally, implement a function called
logEVInfothat accepts anElectricVehicleand 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!
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.