Interfaces
Interfaces in TypeScript: Defining Object Shapes
A TypeScript interface looks simple at first glance, yet many newcomers wonder when to use one and why it matters. If you have worked with plain JavaScript objects for years, the idea of giving those shapes a name can feel like an extra chore. This guide clears up that confusion and shows how interfaces make your code safer and easier to read.
Think of an interface as a small form with labeled fields. Any object that fills in the required fields counts as valid. No magic. No ceremony. You describe the shape and TypeScript checks that your data fits the shape. This alone prevents many silent mistakes in larger projects.
What a TypeScript Interface Actually Is
An interface is a named shape for an object. It lists the properties an object must have and the types of those properties. If an object matches the required structure, TypeScript accepts it. The compiler uses structural typing which means that the object does not need to state that it implements the interface. It simply needs to match it by shape.
Here is a tiny example to ground the concept.
interface Person {
name: string
age: number
}
function greet(p: Person) {
console.log(`Hello ${p.name}`)
}Any object with a name and age fits this shape. If one field is missing, TypeScript warns you early which saves debugging time.
Why Interfaces Matter in Daily Code
Every time you pass data around in an app you rely on assumptions about
shape. Interfaces make those assumptions explicit. When another
developer reads a function like loadUser with parameter user of type
User, they instantly know what structure to expect. This makes
communication and collaboration easier.
Interfaces also allow you to reuse shapes across many parts of the codebase instead of repeating object definitions everywhere. This keeps things simple and prevents drift between different parts of a system.
Extending Interfaces
One of the strongest features of interfaces is that you can extend them.
This lets you build larger shapes from smaller ones, similar to how intersection types combine types with &.
You define a base and layer on new pieces without repeating old fields.
interface BasicInfo {
name: string
age: number
}
interface Address {
city: string
country: string
}
interface Employee extends BasicInfo, Address {
id: number
company: string
}An interface can extend as many parents as you like. Employee above
pulls in every field from both BasicInfo and Address, then adds two
of its own. If two parents declare the same property with incompatible
types, TypeScript rejects the extension instead of silently picking a
winner.
This makes your type system modular and easy to maintain. Any update to the base shapes flows through to all extended shapes.
Optional and Readonly Fields
Real world data is rarely complete and some values must not change after creation. Interfaces support this through optional fields and readonly fields. Optional fields allow flexibility. Readonly fields protect important values like ids from accidental updates.
interface AppConfig {
readonly version: string
themeColor?: string
}This gives your shapes more real world accuracy. When you need to flip
every field to optional or readonly at once, do not hand-write a second
interface. Mapped types such as Partial and
Readonly derive the new shape from the old one for you.
Index Signatures for Dynamic Keys
Sometimes you do not know the property names ahead of time. An index signature describes the key type and the value type instead of listing each field.
interface FeatureFlags {
[flag: string]: boolean
}
const flags: FeatureFlags = {
darkMode: true,
betaSearch: false,
}Any string key is now allowed as long as its value is a boolean. If your keys come from a known set rather than open-ended input, reach for Record instead — it keeps the key list checked, which an index signature cannot do.
Describing Functions and Constructors
Interfaces are not limited to plain data. A call signature lets an interface describe a callable value.
interface Formatter {
(value: number): string
}
const currency: Formatter = (value) => `$${value.toFixed(2)}`Note the missing method name. That bare (value: number): string says
the value itself is callable. Add regular properties alongside it and
you can describe a function that also carries metadata.
A construct signature does the same job for classes, using new.
interface Widget {
id: string
}
interface WidgetConstructor {
new (id: string): Widget
}
function createWidget(ctor: WidgetConstructor, id: string): Widget {
return new ctor(id)
}This is how you type a factory that receives a class and instantiates it rather than receiving an instance.
Generic Interfaces
Hard-coding one concrete type into an interface stops working the moment you need the same shape for something else. A type parameter fixes that.
interface ApiResponse<T> {
data: T
status: number
}
const users: ApiResponse<string[]> = {
data: ['ada', 'grace'],
status: 200,
}One interface now covers every response your API returns. See generics for the full picture of type parameters and constraints.
Declaration Merging
One unique feature of interfaces is declaration merging. If you declare the same interface twice, TypeScript combines them into one. This is useful when extending third party libraries or adding fields incrementally.
interface User {
id: number
}
interface User {
name: string
}
// User now has both id and name
const user: User = { id: 1, name: 'Alice' }Type aliases do not support this. Declaring the same type twice causes an error. This makes interfaces the better choice when you need to augment existing definitions.
Implementing Interfaces in Classes
Interfaces pair naturally with classes. The implements keyword ensures a class provides all required properties and methods.
interface Printable {
print(): void
}
class SalesReport implements Printable {
print() {
console.log('Printing report...')
}
}If the class misses a required member, TypeScript warns you immediately. This pattern is common in larger codebases where you want to enforce contracts across different implementations.
Keep in mind that implements only checks the class. It does not change
the types of the members you write, so each method still needs its own
parameter annotations.
Interfaces vs Types
Type aliases and interfaces often overlap but have different strengths. Interfaces support extension and declaration merging which can be useful for evolving systems. Types shine when you need unions or advanced type features. A simple rule is to reach for interfaces when modeling plain object shapes and reach for types when you need more complex compositions.
Three cases settle the choice quickly:
- Modeling an object that may grow. Use an interface. Extension and merging are built in.
- Modeling a variant, a union, or a tuple. Use a type alias. An interface cannot express
'a' | 'b'. - Deriving a shape from a value you already have. Use a type alias with typeof rather than retyping the shape by hand.
This contrast helps beginners choose without overthinking. Interfaces are great for simple shapes that may grow. Types work better for tricky logic or variant structures.
A Tiny Template for Everyday Use
Here is a small pattern that keeps your code clean.
interface ModelBase {
id: string
createdAt: Date
}
interface UserModel extends ModelBase {
email: string
}Use a base interface for shared fields and extend it for specific models. This keeps duplication low and clarity high.
Common Mistakes
Watch out for these pitfalls when working with interfaces:
- Forgetting that readonly is shallow. Marking a property as
readonlyprevents reassignment but does not freeze nested objects. You can still mutate properties inside a readonly object. - Confusing extends with intersection. Both combine types but behave differently. Extending creates a hierarchy while intersections merge types flatly. Use
extendsfor interfaces and&for type aliases. - Over-specifying when structural typing handles it. You do not need to explicitly implement an interface if the object already matches the shape. TypeScript checks structure, not declarations.
- Missing properties in complex extensions. When extending multiple interfaces, every required property from every parent must exist. Check the full merged shape if you get unexpected errors.
- Reaching for an index signature too early. An open
[key: string]signature turns off the typo protection you wanted in the first place. Only use it when the keys are genuinely unknown.
A quick debugging tip: hover over your interface in the editor to see the fully merged type. This helps catch missing or conflicting fields early.
Wrap Up
Interfaces help you name shapes, communicate intent, and catch errors early. They keep code readable and give structure to the objects you work with every day. Start small. Add one interface to describe a common object in your project and notice how your code becomes clearer immediately.
Have you run into any tricky interface patterns or unexpected behavior? Share your experience with us. We would love to hear how you use interfaces in your projects.
Become a TypeScript Pro
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges