During a recent TypeScript workshop, a developer asked a question I hear all the time: “Should I use type or interface here?”
If you read the docs or AI-generated summaries, you’ll often see “they are mostly the same.” That’s true for simple cases, but it’s not the full story in real-world projects.
In this guide, you’ll learn the practical differences between type and interface in TypeScript, where they behave differently (like declaration merging), and simple rules of thumb you can apply in your own codebase.
We’ll use realistic examples (such as User, Address, and ApiResponse<T>) and cover how teams typically choose between types and interfaces in larger projects.
Quick overview of type vs interface
At a high level:
- Both type and interface can describe the shape of objects.
- Interfaces support declaration merging and the
extendskeyword. - Type aliases support unions, intersections, and advanced combinations.
- In many simple cases, they are interchangeable, but their differences matter as your codebase grows.
A good mental model is:
- Use
interfacefor object shapes you want to model and share. - Use
typefor unions, intersections, and utility “building block” types.
Extendability: extends, intersections, and merging
Extendability is one of the most important differences to understand.
Interface extension with extends
Interfaces can extend other interfaces using the extends keyword, and they can be reopened across files in some scenarios (via declaration merging, covered later).
interface Person {
firstName: string;
lastName: string;
}
interface Employee extends Person {
employeeId: number;
}
const employee: Employee = {
firstName: "Alice",
lastName: "Johnson",
employeeId: 1234,
};I executed the above example code and added the screenshot below.

Here, Employee builds on Person in an object-oriented way.
Type “extension” using intersections
Types cannot be reopened, but you can build on them compositionally using intersections.
interface Person {
firstName: string;
lastName: string;
}
interface Employee extends Person {
employeeId: number;
}
const employee: Employee = {
firstName: "Alice",
lastName: "Johnson",
employeeId: 1234,
};
console.log(employee);This achieves a similar result but via the type system’s intersection operator (&) instead of extends.
Key takeaway
- Interfaces support extends and can also participate in declaration merging.
- Types cannot be reopened, but you can create new types from existing ones using intersections.
Think of interfaces as “named object models” and type aliases as “flexible type expressions.”
Declaration merging (with real-world example)
Declaration merging is something only interfaces can do. It allows multiple declarations of the same interface name to be combined.
Simple example
interface Car {
make: string;
model: string;
}
interface Car {
year: number;
}
const car: Car = {
make: "Tesla",
model: "Model S",
year: 2022,
};
console.log(car); I executed the above example code and added the screenshot below.

Here, both Car declarations are merged into a single interface with make, model, and year.
If you try the same pattern with type, TypeScript will throw an error:
type Car = {
make: string;
};
// Error: Duplicate identifier 'Car'
type Car = {
year: number;
};Real-world library augmentation example
Declaration merging becomes very powerful when you want to extend a third-party type, such as adding a custom property to an Express Request object.
declare module "express-serve-static-core" {
interface Request {
userId?: string;
}
}Because Request is an interface, you can augment it in your own codebase. This is a strong reason to favor interfaces for public APIs and shared domain models.
When to use an interface
Interfaces are usually the best choice when you are modeling the shape of objects, especially if those objects will be shared or extended.
You should consider using an interface when:
- You are defining the shape of domain entities (like
User,Order,Product). - You want other modules, packages, or teams to extend the type later.
- You are working with class-based code or using implements.
- You expect declaration merging or library augmentation.
Example:
interface Address {
street: string;
city: string;
state: string;
zipCode: string;
}
interface User {
id: number;
name: string;
email: string;
address: Address;
}
const user: User = {
id: 1,
name: "John Doe",
email: "john@example.com",
address: {
street: "123 Main St",
city: "Springfield",
state: "IL",
zipCode: "62704",
},
};Here, User and Address are natural uses for interfaces because they represent stable object shapes in your domain.
When to use type
Type aliases shine when you need flexibility beyond plain objects.
You should consider using a type alias when:
- You are defining union types:type Status = “idle” | “loading” | “success” | “error”;
- You are creating intersections or composing multiple types:type Id = string | number; type WithTimestamps = { createdAt: Date; updatedAt: Date; }; type Entity<TId = Id> = { id: TId; } & WithTimestamps;
- You are working with tuples, function types, or more advanced type constructs:type Point = [number, number]; type Callback = (value: string) => void;
- You are defining reusable utility types or wrappers:type ApiResponse<T> = { data: T; error?: string; status: number; };
In short: type aliases are great “building blocks” for complex type structures.
Real-world examples: domain models and API types
Let’s combine interfaces and types in a realistic scenario: modeling a user and an API response.
Domain models with interfaces
interface Address {
street: string;
city: string;
state: string;
zipCode: string;
}
interface User {
id: number;
name: string;
email: string;
address: Address;
}This is a natural fit for interfaces, since User and Address are core domain objects that might be extended or used in multiple places.
API response wrapper with type
type ApiResponse<T> = {
data: T;
error?: string;
status: number;
};
const userResponse: ApiResponse<User> = {
data: {
id: 1,
name: "John Doe",
email: "john@example.com",
address: {
street: "123 Main St",
city: "Springfield",
state: "IL",
zipCode: "62704",
},
},
status: 200,
};Here:
- User is an interface because it models your domain entity.
- ApiResponse<T> is a type alias because it is a generic helper that wraps any type.
Combine interfaces and types
You can also mix both approaches:
interface Person {
firstName: string;
lastName: string;
age: number;
address: Address;
}
interface Employee extends Person {
employeeId: number;
department: string;
}
type Manager = Employee & {
subordinates: Employee[];
};
const manager: Manager = {
firstName: "Alice",
lastName: "Johnson",
age: 40,
address: {
street: "789 Oak St",
city: "Naperville",
state: "IL",
zipCode: "60540",
},
employeeId: 1,
department: "Engineering",
subordinates: [
{
firstName: "Bob",
lastName: "Brown",
age: 35,
address: {
street: "101 Pine St",
city: "Evanston",
state: "IL",
zipCode: "60201",
},
employeeId: 2,
department: "Engineering",
},
],
};I executed the above example code and added the screenshot below.

This example shows how interfaces (Person, Employee) define base shapes, while a type alias (Manager) creates a richer type via intersection.
Best practices and common mistakes
Over time, teams usually settle on a small set of conventions. Here are practical best practices to follow.
Best practices
- Use interfaces for public or shared object models:
- Domain entities (
User,Order,Product). - DTOs and API contracts.
- Types that library consumers might extend.
- Domain entities (
- Use type aliases for:
- Unions (“pending” | “approved” | “rejected”).
- Intersections and composed types.
- Utility types, mapped types, and function signatures.
- Keep it consistent within a project:
- Agree on conventions in your team’s docs or contribution guidelines.
- Prefer predictability over micro-optimizing every type.
Common mistakes
- Treating type and interface as completely identical:
- They behave differently with declaration merging and in some edge cases.
- Trying to merge type aliases:
- Re-declaring the same
typename will cause an error; if you find yourself needing this, you likely want an interface instead.
- Re-declaring the same
- Overcomplicating types:
- Sometimes a simple interface is easier to read and maintain than a deeply nested type alias.
Summary tables
Here is the summary table.
Feature comparison
| Feature | Interface | Type |
|---|---|---|
| Extendability | Yes, can extend other interfaces | Yes, via intersections, but cannot be reopened or merged |
| Declaration merging | Yes, multiple declarations are merged | No, duplicate declarations cause an error |
| Primary usage | Defining object shapes and public models | Unions, intersections, utility types, and non-object types |
| Example use case | User, Address, Order models | Status unions, ApiResponse<T>, function types |
| Combination | Can extend interfaces and reference type aliases | Can intersect with interfaces or other types |
Common scenarios and recommended choice
| Scenario | Recommended | Why |
|---|---|---|
| Public domain models / DTOs | Interface | Easier to extend and familiar for object modeling |
| React props or component inputs | Interface | Common pattern, clear object shapes |
| Union of string literals | Type | Type aliases excel at unions |
| Utility / helper wrapper types | Type | Often involve generics, unions, intersections |
| Augmenting third-party declarations | Interface | Relies on declaration merging |
How do I explain this in live workshops
When teaching TypeScript to teams, I start with a simple question: “Who will consume this type?”
If the answer is “other teams, other modules, or external packages,” we default to interface because it is easier to extend and supports declaration merging.
For internal plumbing, helpers, and unions, we freely use type aliases, especially where unions and intersections make the intent clearer.
This mental model helps developers stop overthinking and pick one quickly.
Quick quiz: test your understanding
Try answering these without looking back, then check yourself at the end.
- You need a Status that can be “success” | “error” | “loading”. Do you use
typeor interface? - You are defining a shared UserProfile that other teams will extend in their modules. Type or interface?
- You want a generic Paginated<T> wrapper with properties items, total, and page. Type or interface?
Suggested answers:
- type (it’s a union of string literals, not an object shape).
- interface (a shared object model that others may extend).
- Usually type, especially if you plan to compose it with other types, though the interface can also work here.
Conclusion
In practice, the real question isn’t simply “type or interface?” but “how will this type evolve, and who will depend on it?”
Use interfaces for stable, extendable object models and public contracts, and use type aliases for unions, intersections, and utility helpers.
If you follow that rule of thumb and understand declaration merging and extendability, you’ll be able to choose confidently instead of guessing every time you define a new type.
You may also read:
- Check If Object Is Undefined in TypeScript
- Check if an Object Has a Property in TypeScript
- Mapped Types in TypeScript
- Typescript Iterate Over Records

Bijay Kumar is an experienced Python and AI professional who enjoys helping developers learn modern technologies through practical tutorials and examples. His expertise includes Python development, Machine Learning, Artificial Intelligence, automation, and data analysis using libraries like Pandas, NumPy, TensorFlow, Matplotlib, SciPy, and Scikit-Learn. At PythonGuides.com, he shares in-depth guides designed for both beginners and experienced developers. More about us.