Interface vs Class in Angular: When and Why
The choice between interfaces and classes depends on what you need to accomplish. Let me break this down:
Key Differences
Interface | Class |
---|---|
Compile-time only | Runtime available |
Type checking & contracts | Can be instantiated |
No implementation | Can have implementation |
Multiple inheritance | Single inheritance |
Zero JavaScript output | Generates JavaScript |
When to Use Interfaces
✅ Use Interfaces For:
1. Data Models/DTOs (Most Common)
// API response structure
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
@Injectable()
export class UserService {
getUser(id: number): Observable<ApiResponse<User>> {
return this.http.get<ApiResponse<User>>(`/api/users/${id}`);
}
}
2. Component Input/Output Contracts
interface UserCardConfig {
showAvatar: boolean;
showEmail: boolean;
clickable: boolean;
}
@Component({
selector: 'app-user-card',
template: `...`
})
export class UserCardComponent {
@Input() user: User;
@Input() config: UserCardConfig;
@Output() userClick = new EventEmitter<User>();
}
3. Service Contracts (Dependency Injection)
interface IUserService {
getUser(id: number): Observable<User>;
updateUser(user: User): Observable<User>;
deleteUser(id: number): Observable<void>;
}
@Injectable()
export class UserService implements IUserService {
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
updateUser(user: User): Observable<User> {
return this.http.put<User>(`/api/users/${user.id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`/api/users/${id}`);
}
}
// Easy to mock for testing
export class MockUserService implements IUserService {
getUser(id: number): Observable<User> {
return of({ id, name: 'Test User', email: 'test@test.com' });
}
// ... other methods
}
4. Configuration Objects
interface AppConfig {
apiUrl: string;
timeout: number;
retryAttempts: number;
features: {
enableLogging: boolean;
enableAnalytics: boolean;
};
}
const config: AppConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retryAttempts: 3,
features: {
enableLogging: true,
enableAnalytics: false
}
};
When to Use Classes
✅ Use Classes For:
1. Business Logic Models
export class ShoppingCart {
private items: CartItem[] = [];
constructor(private userId: string) {}
addItem(product: Product, quantity: number): void {
const existingItem = this.items.find(item => item.productId === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push(new CartItem(product, quantity));
}
}
removeItem(productId: string): void {
this.items = this.items.filter(item => item.productId !== productId);
}
get totalPrice(): number {
return this.items.reduce((total, item) => total + item.totalPrice, 0);
}
get itemCount(): number {
return this.items.reduce((count, item) => count + item.quantity, 0);
}
}
export class CartItem {
constructor(
public product: Product,
public quantity: number
) {}
get totalPrice(): number {
return this.product.price * this.quantity;
}
}
2. Data Transformation/Validation
export class User {
constructor(
public id: number,
public firstName: string,
public lastName: string,
public email: string,
public birthDate: Date
) {}
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
get age(): number {
const today = new Date();
const birth = new Date(this.birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
}
isAdult(): boolean {
return this.age >= 18;
}
static fromApiResponse(data: any): User {
return new User(
data.id,
data.first_name,
data.last_name,
data.email,
new Date(data.birth_date)
);
}
}
// Usage in service
@Injectable()
export class UserService {
getUser(id: number): Observable<User> {
return this.http.get(`/api/users/${id}`).pipe(
map(data => User.fromApiResponse(data))
);
}
}
3. Complex State Management
export class FormState {
private _errors: { [key: string]: string[] } = {};
private _touched: { [key: string]: boolean } = {};
constructor(private _values: { [key: string]: any } = {}) {}
setValue(field: string, value: any): void {
this._values[field] = value;
this.validateField(field);
}
getValue(field: string): any {
return this._values[field];
}
setTouched(field: string): void {
this._touched[field] = true;
}
getErrors(field: string): string[] {
return this._errors[field] || [];
}
isValid(): boolean {
return Object.keys(this._errors).length === 0;
}
private validateField(field: string): void {
// Validation logic
const value = this._values[field];
const errors: string[] = [];
if (!value) {
errors.push(`${field} is required`);
}
this._errors[field] = errors;
if (errors.length === 0) {
delete this._errors[field];
}
}
}
4. Services with State
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notifications: Notification[] = [];
private notificationSubject = new BehaviorSubject<Notification[]>([]);
notifications$ = this.notificationSubject.asObservable();
addNotification(message: string, type: 'success' | 'error' | 'info' = 'info'): void {
const notification = new Notification(message, type);
this.notifications.push(notification);
this.notificationSubject.next([...this.notifications]);
// Auto remove after 5 seconds
setTimeout(() => this.removeNotification(notification.id), 5000);
}
removeNotification(id: string): void {
this.notifications = this.notifications.filter(n => n.id !== id);
this.notificationSubject.next([...this.notifications]);
}
}
export class Notification {
id: string;
timestamp: Date;
constructor(
public message: string,
public type: 'success' | 'error' | 'info'
) {
this.id = Math.random().toString(36).substr(2, 9);
this.timestamp = new Date();
}
}
Hybrid Approach (Common Pattern)
Often you'll use both together:
// Interface for type safety
interface IProduct {
id: string;
name: string;
price: number;
category: string;
}
// Class for business logic
export class Product implements IProduct {
constructor(
public id: string,
public name: string,
public price: number,
public category: string
) {}
get formattedPrice(): string {
return `$${this.price.toFixed(2)}`;
}
isInCategory(category: string): boolean {
return this.category.toLowerCase() === category.toLowerCase();
}
applyDiscount(percentage: number): number {
return this.price * (1 - percentage / 100);
}
}
// Service using both
@Injectable()
export class ProductService {
// Returns interface type for API data
getProductData(id: string): Observable<IProduct> {
return this.http.get<IProduct>(`/api/products/${id}`);
}
// Returns class instance with methods
getProduct(id: string): Observable<Product> {
return this.getProductData(id).pipe(
map(data => new Product(data.id, data.name, data.price, data.category))
);
}
}
Decision Guidelines
Use Interface when you need:
- ✅ Type checking only
- ✅ Data contracts/shapes
- ✅ API response types
- ✅ Component input/output types
- ✅ Configuration objects
- ✅ Multiple inheritance
Use Class when you need:
- ✅ Methods and behavior
- ✅ Computed properties
- ✅ Data transformation
- ✅ Business logic
- ✅ State management
- ✅ Runtime type checking
- ✅ Instantiation with
new
Use Both when you need:
- ✅ Type safety + behavior
- ✅ API contracts + business logic
- ✅ Flexible implementation options
The key is: Interfaces define shape, Classes define behavior!