When working with TypeScript, one of the most common questions you'll encounter—whether in interviews or day-to-day coding—is how to decide between using interface and type. Both are powerful tools for defining shapes of data, but they have subtle differences that can impact maintainability, scalability, and developer experience. Over the years, I've seen developers struggle with this choice, often defaulting to one without understanding the trade-offs.
In this answer, I’ll walk through the core concepts behind interface and type, practical examples from real projects, common pitfalls, and some guidance on when to prefer one over the other. This should help you not only answer this question confidently in interviews but also write cleaner, more maintainable TypeScript code.
interface and typeAt a high level, both interface and type aliases let you describe the shape of an object or a function signature. However, they are not interchangeable in every scenario.
Here’s a quick example to illustrate the syntax:
interface User {
id: number;
name: string;
}
type UserType = {
id: number;
name: string;
};
Both define the same shape, but the capabilities beyond this simple example start to diverge.
One of the unique features of interface is declaration merging. This means you can declare the same interface multiple times, and TypeScript will merge their members. This is especially useful when extending types from third-party libraries or gradually adding properties.
interface User {
id: number;
}
interface User {
name: string;
}
// Equivalent to:
// interface User {
// id: number;
// name: string;
// }
On the other hand, type aliases don’t allow this. If you try to declare the same type twice, you’ll get a compiler error.
Type aliases shine when you need to compose types using unions (|) and intersections (&), which interfaces don’t support as flexibly.
type Status = "success" | "error" | "loading";
type User = {
id: number;
name: string;
};
type Admin = User & {
adminSince: Date;
};
Interfaces can extend other interfaces, but you can’t create union types with interfaces. This makes type more versatile for complex type compositions.
In my experience working on large-scale React and Node.js applications, I tend to use interface when defining public API contracts, especially for objects and classes. This is because interfaces are more explicit and support declaration merging, which helps when extending or augmenting types across modules.
For example, when defining props for a React component, interfaces work well:
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({ label, onClick, disabled }) => {
return <button onClick={onClick} disabled={disabled}>{label}</button>;
};
On the other hand, I use type aliases when dealing with unions, intersections, or more complex types that go beyond just object shapes. For instance, when modeling API response statuses or discriminated unions:
type ApiResponse =
| { status: "success"; data: User[] }
| { status: "error"; error: string };
This pattern is common in Redux or React Query states, where you want to represent mutually exclusive states clearly.
Interfaces are designed to be implemented by classes, which makes them a natural fit for object-oriented patterns:
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string) {
console.log(message);
}
}
While you technically can use type aliases with classes, it’s less idiomatic and can lead to confusion.
type for everything: Some developers default to type because it feels more flexible, but this can hurt readability and maintainability, especially in large teams where explicit contracts are preferred.interface and type without a clear convention can confuse team members and complicate refactoring.From a TypeScript compiler perspective, there’s no significant runtime difference between interface and type because both are erased during compilation. However, the choice impacts developer experience and scalability of your codebase.
Interfaces tend to be easier to extend and maintain in large projects, especially when working with third-party libraries or when you expect your types to grow over time. Declaration merging allows you to add properties without modifying original declarations, which is handy in modular codebases.
Type aliases, while more flexible, can become unwieldy if overused for complex unions or intersections, making the type system harder to navigate and increasing cognitive load.
While interfaces and types themselves don’t directly affect runtime security, clear and precise type definitions can prevent bugs that lead to security vulnerabilities. For example, defining strict types for API inputs helps catch invalid data early, reducing injection risks or unexpected behavior.
Using discriminated unions with type aliases can enforce exhaustive checks in switch statements, ensuring all cases are handled and reducing the chance of unhandled error states.
interface vs type| Aspect | interface |
type |
|---|---|---|
| Primary Use | Defining object shapes and contracts | Defining object shapes, unions, intersections, primitives |
| Declaration Merging | Supported | Not supported |
| Extending | Can extend and be extended by other interfaces | Can create intersections but no merging |
| Unions | Not supported | Fully supported |
| Implementable by Classes | Yes | Technically yes, but not idiomatic |
| Use in Large Codebases | Better for public APIs and extensibility | Better for complex type compositions |
Choosing between interface and type isn’t about one being better than the other—it's about picking the right tool for the job. Interfaces excel when you want clear, extendable contracts, especially for objects and classes. Type aliases are your go-to for unions, intersections, and more complex or flexible types.
In production, consistency matters. Many teams adopt a convention like “use interfaces for object shapes and public APIs, use types for unions and intersections.” This keeps the codebase predictable and easier to maintain.
When preparing for interviews, focus on demonstrating your understanding of these nuances, show practical examples, and explain the reasoning behind your choices. That’s what sets experienced developers apart.