Aller au contenu principal

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

InterfaceClass
Compile-time onlyRuntime available
Type checking & contractsCan be instantiated
No implementationCan have implementation
Multiple inheritanceSingle inheritance
Zero JavaScript outputGenerates 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!