Where They Look the Same
For the most common use case — describing an object shape — both are syntactically close and produce identical type-checking behaviour:
interface User {
id: string;
name: string;
email: string;
age?: number;
}type User = {
id: string;
name: string;
email: string;
age?: number;
}Both support optional properties (?), readonly modifiers, method signatures, generics, and extension. For object shapes in application code, pick one and be consistent.
Difference 1: Declaration Merging
interface supports declaration merging — declaring the same interface name twice merges the definitions. type does not allow redeclaration:
interface Window {
myCustomProp: string;
}
// Now window.myCustomProp is typed ✅
// This is how DefinitelyTyped augments browser globals
type Window = { myCustomProp: string }
// Error: Duplicate identifier 'Window'. ❌Declaration merging is essential for module augmentation — extending third-party library types, global DOM augmentation, or adding properties to Express's Request type. This capability is exclusive to interface.
Difference 2: Union and Intersection Types
type can represent union types, intersection types, and mapped types directly. interface cannot:
// Union — only possible with type
type Status = "pending" | "active" | "archived";
type ID = string | number;
type Result<T> = { data: T } | { error: string };
// Intersection (type alias)
type AdminUser = User & Admin;
// interface can extend (close equivalent)
interface AdminUser extends User, Admin {}
// But interface extends fails for union membersDifference 3: Computed and Mapped Types
Computed property keys and mapped types are exclusively available in type:
// Mapped type — type only
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Conditional type — type only
type NonNullable<T> = T extends null | undefined ? never : T;TypeScript's built-in utility types (Partial<T>, Required<T>, Pick<T,K>, Omit<T,K>) are all implemented as mapped type aliases for this reason.
Difference 4: Extending Syntax
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}type Animal = { name: string };
type Dog = Animal & {
breed: string;
};Both achieve similar results. The key difference: interface extends will error if the extended type has incompatible property types. type & intersection will silently produce never for the conflicting property — which can cause hard-to-debug errors.
Difference 5: Error Message Quality
Interfaces produce cleaner error messages. TypeScript inlines type aliases in errors, which can make complex types extremely verbose in the output. For large teams, this is a real ergonomics consideration when choosing for domain model types.
The Definitive Decision Table
| Capability | interface | type |
|---|---|---|
| Object shape definition | Yes | Yes |
| Optional / readonly properties | Yes | Yes |
| Generics | Yes | Yes |
| Extension / composition | Yes (extends) | Yes (&) |
| Declaration merging | Yes | No |
| Module augmentation | Yes | No |
| Union types | No | Yes |
| Intersection types | No (use extends) | Yes |
| Mapped types | No | Yes |
| Conditional types | No | Yes |
| Tuple types | No | Yes |
| Primitive aliases | No | Yes |
Practical Recommendation
- Use
interfacefor: domain model types (User, Order, Product), class contracts (implements), public library APIs that consumers may need to extend via declaration merging. - Use
typefor: union types, intersection compositions of multiple types, utility type definitions, any type that is not purely an object shape (primitives, tuples, functions). - Pick one for your "plain object shape" standard and document it in your team's style guide. Consistency matters more than the specific choice.