Interfaces vs Types in TypeScript
What is the difference between TypeScript interfaces and type aliases? When should I use one over the other?
interface X {
a: number
b: string
}
type X = {
a: number
b: string
};
I’m looking for a clear explanation of the key differences between these two TypeScript features, including their syntax, capabilities, and best use cases.
TypeScript interfaces and type aliases serve similar purposes in defining shapes and structures, but they have distinct capabilities and behaviors. Interfaces can be extended, support declaration merging, and are primarily designed for object shapes, while type aliases offer greater flexibility for representing primitives, unions, tuples, and other complex types without merging capabilities.
Contents
- Core Syntax Differences
- Declaration Merging
- Extending and Implementation
- Type Capabilities
- Best Use Cases
- Performance Considerations
Core Syntax Differences
The basic syntax for defining object shapes appears similar at first glance, but there are important distinctions:
// Interface syntax
interface User {
id: number;
name: string;
email: string;
}
// Type alias syntax
type User = {
id: number;
name: string;
email: string;
};
Key syntax differences:
- Interfaces use the
interfacekeyword and are more declarative - Type aliases use the
typekeyword and can represent a much wider range of types - Both support optional properties (
?), readonly properties (readonly), and method signatures
// Both support the same object features
interface Person {
readonly id: number; // readonly property
name: string;
age?: number; // optional property
greet(): string; // method signature
}
type Person = {
readonly id: number;
name: string;
age?: number;
greet(): string;
};
Interfaces can also be named or anonymous, while type aliases are always named declarations.
Declaration Merging
This is one of the most significant differences between interfaces and type aliases.
Interfaces support declaration merging - multiple interface declarations with the same name are automatically merged into a single interface:
interface User {
id: number;
name: string;
}
interface User {
email: string;
}
// Resulting interface has all properties
const user: User = {
id: 1,
name: "John",
email: "john@example.com" // This works due to declaration merging
};
Type aliases do NOT support declaration merging - attempting to declare multiple type aliases with the same name will result in a compilation error:
type User = {
id: number;
name: string;
};
// Error: Duplicate identifier 'User'
type User = {
email: string;
};
Extending and Implementation
Interfaces provide inheritance capabilities that type aliases cannot match.
Interface Extension
Interfaces can extend other interfaces using the extends keyword:
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const dog: Dog = {
name: "Rex",
breed: "German Shepherd",
bark() { console.log("Woof!"); }
};
Multiple Interface Extension
An interface can extend multiple interfaces:
interface Walkable {
walk(): void;
}
interface Swimmable {
swim(): void;
}
interface Amphibious extends Walkable, Swimmable {
name: string;
}
Type Alias Extension
Type aliases can achieve similar functionality using intersection types:
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
bark(): void;
};
Class Implementation
Only interfaces can be implemented by classes:
interface Serializable {
serialize(): string;
}
class User implements Serializable {
constructor(public id: number, public name: string) {}
serialize(): string {
return JSON.stringify({ id: this.id, name: this.name });
}
}
Type aliases cannot be implemented by classes, making interfaces essential for object-oriented programming patterns in TypeScript.
Type Capabilities
This area shows the most dramatic difference between interfaces and type aliases.
What Interfaces Can Represent
Interfaces are primarily designed for object shapes and class structures:
// Object shapes
interface Point {
x: number;
y: number;
}
// Class structures
interface Serializable {
serialize(): string;
}
// Function types (less common)
interface Callback {
(error: Error | null, data: string): void;
}
What Type Aliases Can Represent
Type aliases offer much greater flexibility and can represent almost any TypeScript type:
// Primitives
type ID = string | number;
// Unions
type Result = 'success' | 'error' | 'pending';
// Tuples
type Coordinates = [number, number, number];
// Mapped types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Conditional types
type ExtractType<T> = T extends string ? 'string' : 'not string';
// Template literal types
type EventName = `on${'Click' | 'Hover' | 'Focus'}`;
// Complex combinations
type Complex = string | number[] | { data: any } | (() => void);
Generic Constraints
Both interfaces and type aliases support generics, but with different syntax:
// Interface generics
interface Repository<T> {
findById(id: string): T | null;
save(entity: T): void;
}
// Type alias generics
type Repository<T> = {
findById(id: string): T | null;
save(entity: T): void;
};
Best Use Cases
When to Use Interfaces
Choose interfaces when:
- Defining object shapes for APIs, data models, or configuration objects
- Creating contracts for classes to implement
- Needing extension through inheritance
- Working with third-party libraries that expect interface definitions
- Requiring declaration merging for augmenting existing types
- Writing object-oriented code with class hierarchies
// Good interface usage
interface ApiResponse {
success: boolean;
data: any;
timestamp: Date;
}
// Class implementation
class ApiClient implements ApiResponse {
// ... implementation
}
When to Use Type Aliases
Choose type aliases when:
- Representing primitives, unions, tuples, or other non-object types
- Creating complex type compositions using intersection or union types
- Need to define conditional or mapped types
- Working with template literal types
- Wanting to prevent unintended type merging
- Creating utility types or type helpers
// Good type alias usage
type UserId = string | number;
type Status = 'active' | 'inactive' | 'pending';
type Coordinates = [number, number];
type ApiResponse<T> = T extends 'success' ? SuccessResponse : ErrorResponse;
Hybrid Approach
Many TypeScript projects use both approaches strategically:
// Use interfaces for object shapes
interface User {
id: string;
name: string;
email: string;
}
// Use type aliases for compositions
type UserProfile = User & {
preferences: UserPreferences;
settings: UserSettings;
};
// Type alias for complex logic
type FilterableUser = User & {
matches(filter: UserFilter): boolean;
};
Performance Considerations
Compilation Performance
For most applications, the performance difference between interfaces and type aliases is negligible. However, there are some considerations:
- Interfaces with declaration merging can sometimes lead to larger type definitions
- Complex type aliases with deep nesting or conditional types may compile slower
- Generic interfaces can have performance implications in large codebases
Bundle Size
At runtime, both interfaces and type aliases are erased from the compiled JavaScript. The choice between them doesn’t affect bundle size.
Type Checking Performance
In very large codebases, interfaces with extensive declaration merging might slightly impact type checking performance compared to type aliases.
Tooling and IDE Support
Modern TypeScript tools and IDEs handle both interfaces and type aliases very well. However, some advanced features like “Go to Definition” may work slightly differently for each.
Conclusion
Key Takeaways
-
Interfaces excel at object-oriented patterns - they support extension, implementation, and declaration merging, making them ideal for defining contracts and class hierarchies.
-
Type aliases offer greater flexibility - they can represent primitives, unions, tuples, and complex type compositions that interfaces cannot handle.
-
Declaration merging is the biggest differentiator - interfaces merge automatically, while type aliases reject duplicate declarations.
-
Both are erased at runtime - your choice doesn’t affect JavaScript output or bundle size.
Practical Recommendations
- Start with interfaces for most object definitions, especially when working with classes
- Use type aliases for complex types, unions, tuples, and utility types
- Avoid mixing approaches unnecessarily - be consistent within your codebase
- Leverage both when appropriate - interfaces for shapes, type aliases for compositions
When in Doubt
If you’re unsure which to use, consider these questions:
- Do I need to extend this or implement it in a class? → Use interface
- Am I defining a complex type composition? → Use type alias
- Do I want multiple declarations to merge automatically? → Use interface
- Am I representing a primitive, union, or tuple? → Use type alias