TypeScript: Types vs Interfaces - A Comparison Guide
/ 5 min read
TypeScript: Types vs Interfaces vs Classes
One of the most common questions TypeScript developers face is when to use types, interfaces, or classes. This guide will help you make informed decisions by comparing these features and providing clear guidelines for their usage.
Table of Contents
Quick Comparison
Here’s a quick overview of the key differences:
// Type Aliastype Point = { x: number; y: number;};
// Interfaceinterface Point { x: number; y: number;}
// Classclass Point { constructor( public x: number, public y: number ) {}}Key Differences
- Declaration Merging
// Interfaces can be mergedinterface User { name: string;}interface User { age: number;}// Results in: interface User { name: string; age: number; }
// Types cannot be mergedtype User = { name: string;}// Error: Duplicate identifier 'User'type User = { age: number;}- Computed Properties
// Types can use computed propertiestype Keys = "firstname" | "lastname";type DuplicateString<K extends string> = { [P in K]: string;}type NameFields = DuplicateString<Keys>;// { firstname: string; lastname: string; }
// Interfaces cannot use computed properties directly- Union Types
// Types can be unionstype Status = "pending" | "approved" | "rejected";
// Interfaces cannot be unionsinterface Status { /* Error */ }- Implementation and Inheritance
// Classes can implement interfacesinterface Animal { name: string; makeSound(): void;}
class Dog implements Animal { constructor(public name: string) {} makeSound() { console.log("Woof!"); }}
// Classes can extend other classesclass Shape { constructor(public color: string) {}}
class Circle extends Shape { constructor(color: string, public radius: number) { super(color); }}When to Use Each
Use Types When
- Creating Union Types
type Result<T> = { success: true; data: T;} | { success: false; error: string;};
function processResult<T>(result: Result<T>) { if (result.success) { // TypeScript knows result.data exists console.log(result.data); } else { // TypeScript knows result.error exists console.log(result.error); }}- Working with Tuples
type HttpResponse = [number, string, any];type Coordinates = [number, number];
const response: HttpResponse = [200, "OK", { data: "..." }];const point: Coordinates = [10, 20];- Creating Complex Type Manipulations
type Nullable<T> = T | null;type Readonly<T> = { readonly [P in keyof T]: T[P];};type Pick<T, K extends keyof T> = { [P in K]: T[P];};Use Interfaces When
- Defining Object Shapes
interface User { id: string; name: string; email: string;}
interface UserService { getUser(id: string): Promise<User>; updateUser(user: User): Promise<void>;}- Working with Classes
interface Repository<T> { find(id: string): Promise<T>; save(item: T): Promise<void>; delete(id: string): Promise<boolean>;}
class UserRepository implements Repository<User> { // Implementation}- Extending Other Interfaces
interface BaseEntity { id: string; createdAt: Date; updatedAt: Date;}
interface User extends BaseEntity { name: string; email: string;}Use Classes When
- Creating Instances with State and Behavior
class Counter { private count: number = 0;
increment(): void { this.count++; }
getCount(): number { return this.count; }}
const counter = new Counter();counter.increment();- Implementing Object-Oriented Patterns
class Logger { private static instance: Logger; private constructor() {}
static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; }
log(message: string): void { console.log(message); }}- Managing Complex State with Encapsulation
class ShoppingCart { private items: Array<{ id: string; quantity: number; price: number; }> = [];
addItem(id: string, quantity: number, price: number): void { this.items.push({ id, quantity, price }); }
getTotal(): number { return this.items.reduce( (total, item) => total + item.quantity * item.price, 0 ); }}Best Practices
- Prefer Interfaces for Public APIs
// Goodinterface ApiResponse<T> { data: T; status: number; message: string;}
// Instead oftype ApiResponse<T> = { data: T; status: number; message: string;};- Use Types for Complex Type Operations
// Goodtype NonNullableFields<T> = { [P in keyof T]: NonNullable<T[P]>;};
// Instead of trying to achieve this with interfaces- Use Classes for Stateful Objects
// Goodclass UserManager { private users: Map<string, User> = new Map();
addUser(user: User): void { this.users.set(user.id, user); }}
// Instead ofinterface UserManager { users: Map<string, User>; addUser(user: User): void;}Common Patterns
Combining Types and Interfaces
// Define base shape with interfaceinterface BaseEntity { id: string; createdAt: Date;}
// Create union type with interfacetype EntityType = "user" | "product" | "order";
// Combine in a new interfaceinterface Entity extends BaseEntity { type: EntityType;}Using Classes with Interfaces
interface Observable<T> { subscribe(observer: (value: T) => void): void; unsubscribe(observer: (value: T) => void): void; notify(value: T): void;}
class DataStream<T> implements Observable<T> { private observers: ((value: T) => void)[] = [];
subscribe(observer: (value: T) => void): void { this.observers.push(observer); }
unsubscribe(observer: (value: T) => void): void { this.observers = this.observers.filter(obs => obs !== observer); }
notify(value: T): void { this.observers.forEach(observer => observer(value)); }}Conclusion
Choose based on your needs:
-
Types for:
- Union types
- Tuple types
- Complex type manipulations
- Mapped types
- Utility types
-
Interfaces for:
- Object shapes
- API contracts
- Class contracts
- Extendable definitions
- Declaration merging
-
Classes for:
- Object instances
- Encapsulation
- Inheritance
- Object-oriented patterns
- Stateful behavior
Remember:
- Interfaces are often preferred for public APIs due to their extensibility
- Types are great for complex type manipulations and unions
- Classes are best when you need instances with behavior and state
- You can combine these features to create more powerful and flexible code