40.1 Prefer Interfaces for Public API, Type Aliases for Complex Compositions
Right, let’s settle this. You’ve probably seen interface and type used seemingly interchangeably all over the place, and for simple object shapes, they basically are. But they’re not the same tool, and using them correctly is a mark of someone who knows their stuff. The golden rule I live by, and the one that will save you from future headaches, is this: Use interface for anything that forms the public contract of your code (especially for object inheritance), and use type for complex compositions, unions, and mappings.
Here’s why this isn’t just pedantry.
The Core Difference: Declaration Merging
This is the big one. An interface in TypeScript is an open declaration. You can declare it multiple times, and TypeScript will happily merge all those declarations into a single definition. A type alias, on the other hand, is a closed definition. You can’t have two identical type declarations in the same scope; it’ll yell at you for the duplicate identifier.
This makes interface the undisputed champion for extending library and third-party types, which is a cornerstone of a healthy ecosystem.
// Imagine you're using a library that defines a `User` interface.
// You can easily add a property for your app's specific needs.
declare module 'some-library' {
interface User {
myCustomField: string; // Augment the existing interface
}
}
const user: User = getUserFromLibrary();
user.myCustomField = "This works!"; // TypeScript knows about it now.
// You simply cannot do this with a type alias.
type User = { // Error: Duplicate identifier 'User'
myCustomField: string;
}
This is precisely why public APIs (like the types you export from your library) should be interfaces. It gives your users an escape hatch. If your types are slightly incomplete or they need to add some plugin-specific field, they can. You’ve given them power. If you export a type, you’ve slammed the door shut. Be a good citizen; export interfaces.
Where Type Aliases Shine: Composition and Logic
While interface is busy being great at object inheritance, type is the wizard you call upon for complex type logic. You can’t create a union or an intersection with an interface.
// This is the domain of 'type'. An interface can't do this.
type ID = string | number; // Simple union
type HttpStatusCode = 200 | 201 | 400 | 404 | 500; // Union of literals
// Complex compositions are where 'type' is irreplaceable.
type AdminUser = User & { // Intersection
permissions: string[];
};
type ApiResponse<T> = { // Generic mapped type
data: T;
status: HttpStatusCode;
pagination?: {
page: number;
total: number;
};
};
Trying to hack that together with interface would be like using a screwdriver to pound in a nail—messy, ineffective, and frankly, a bit sad.
The Performance Myth (And the Real One)
You might hear someone mutter in a dark corner of the internet that interface is “faster” for the compiler than type because it can be cached by its name. Let’s be direct: this is almost certainly a micro-optimization that will have zero measurable impact on your codebase. The real performance consideration is human performance.
The real pitfall is picking the wrong tool and making your own life, or the lives of your teammates, harder. If you use a type for a public API, you remove the ability for others to extend it. If you try to use an interface for a union type, you’ll just get a syntax error and waste time. Use the right tool for the job.
So, What About extends vs &?
This is where people get tripped up. For object types, you can often achieve the same result, but the intent is different.
interface A {
a: string;
}
interface B extends A { // "B is a more specific A"
b: number;
}
type C = {
c: boolean;
} & A; // "C is the combination of this object and A"
// The result is similar, but error messages can differ.
// Let's create an error on purpose:
interface BadB extends { c: boolean } { // Error: An interface can only extend an object type.
b: number;
}
type BadC = A & number; // This is allowed! It resolves to `never` in practice, but it compiles.
The & (intersection) operator is more permissive and can lead to creating impossible types (like A & number), which resolve to never. The extends keyword in an interface is more strict and will only let you extend something that is actually a valid object type. This is another reason interface is safer for building up object hierarchies.
The bottom line? Default to interface for defining object shapes you expect to be implemented or extended, especially if they’re part of your public-facing contract. Reach for type when you’re doing logic, creating unions, or building complex compositions. It’s not a holy war; it’s about using two excellent tools for their intended purposes.