TypeScript Interfaces: A Complete Guide
/ 7 min read
TypeScript Interfaces: A Complete Guide
Interfaces are one of TypeScript’s most powerful features, allowing you to define contracts in your code and providing explicit names for type checking. This comprehensive guide covers everything you need to know about TypeScript interfaces with real-world examples.
Table of Contents
- Basic Interface Declaration
- Optional and Readonly Properties
- Function Types
- Indexable Types
- Extending Interfaces
- Implementing Interfaces
- Real-World Examples
- Best Practices
Basic Interface Declaration
Let’s start with a real-world example of a user management system:
// Basic user interfaceinterface User { id: string; username: string; email: string; createdAt: Date; lastLogin?: Date;}
// User service interfaceinterface UserService { findById(id: string): Promise<User>; create(user: Omit<User, 'id' | 'createdAt'>): Promise<User>; update(id: string, user: Partial<User>): Promise<User>; delete(id: string): Promise<boolean>;}
// Implementation exampleclass UserServiceImpl implements UserService { private users: Map<string, User> = new Map();
async findById(id: string): Promise<User> { const user = this.users.get(id); if (!user) throw new Error('User not found'); return user; }
async create(userData: Omit<User, 'id' | 'createdAt'>): Promise<User> { const newUser: User = { ...userData, id: crypto.randomUUID(), createdAt: new Date() }; this.users.set(newUser.id, newUser); return newUser; }
async update(id: string, userData: Partial<User>): Promise<User> { const existingUser = await this.findById(id); const updatedUser = { ...existingUser, ...userData }; this.users.set(id, updatedUser); return updatedUser; }
async delete(id: string): Promise<boolean> { return this.users.delete(id); }}Real-World API Interface Examples
RESTful API Client Interface
// API Response interfacesinterface ApiResponse<T> { data: T; metadata: { timestamp: number; status: number; message: string; };}
interface PaginatedResponse<T> extends ApiResponse<T[]> { metadata: { timestamp: number; status: number; message: string; pagination: { currentPage: number; pageSize: number; totalPages: number; totalItems: number; }; };}
// API Client interfaceinterface ApiClient { get<T>(url: string, params?: Record<string, string>): Promise<ApiResponse<T>>; post<T, U>(url: string, data: T): Promise<ApiResponse<U>>; put<T, U>(url: string, data: T): Promise<ApiResponse<U>>; delete(url: string): Promise<ApiResponse<void>>;}
// Implementation exampleclass HttpApiClient implements ApiClient { constructor(private baseUrl: string, private apiKey: string) {}
private async request<T>( url: string, options: RequestInit ): Promise<ApiResponse<T>> { const response = await fetch(`${this.baseUrl}${url}`, { ...options, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, ...options.headers, }, });
if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); }
return response.json(); }
async get<T>(url: string, params?: Record<string, string>): Promise<ApiResponse<T>> { const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''; return this.request<T>(`${url}${queryString}`, { method: 'GET' }); }
async post<T, U>(url: string, data: T): Promise<ApiResponse<U>> { return this.request<U>(url, { method: 'POST', body: JSON.stringify(data), }); }
async put<T, U>(url: string, data: T): Promise<ApiResponse<U>> { return this.request<U>(url, { method: 'PUT', body: JSON.stringify(data), }); }
async delete(url: string): Promise<ApiResponse<void>> { return this.request(url, { method: 'DELETE' }); }}E-commerce System Interfaces
// Product management interfacesinterface Product { id: string; name: string; description: string; price: number; category: string; stock: number; images: string[];}
interface CartItem { productId: string; quantity: number; price: number;}
interface ShoppingCart { id: string; userId: string; items: CartItem[]; subtotal: number; tax: number; total: number;}
interface Order extends Omit<ShoppingCart, 'id'> { id: string; orderNumber: string; status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'; shippingAddress: Address; billingAddress: Address; paymentDetails: PaymentDetails; createdAt: Date; updatedAt: Date;}
interface Address { street: string; city: string; state: string; postalCode: string; country: string;}
interface PaymentDetails { method: 'credit_card' | 'paypal' | 'bank_transfer'; status: 'pending' | 'completed' | 'failed'; transactionId?: string;}
// E-commerce service interfacesinterface ProductService { findById(id: string): Promise<Product>; search(query: string, category?: string): Promise<Product[]>; updateStock(id: string, quantity: number): Promise<void>;}
interface CartService { getCart(userId: string): Promise<ShoppingCart>; addItem(userId: string, productId: string, quantity: number): Promise<void>; removeItem(userId: string, productId: string): Promise<void>; updateQuantity(userId: string, productId: string, quantity: number): Promise<void>; checkout(userId: string): Promise<Order>;}
// Implementation example of cart serviceclass CartServiceImpl implements CartService { constructor( private productService: ProductService, private cartRepository: Repository<ShoppingCart>, private orderRepository: Repository<Order> ) {}
async getCart(userId: string): Promise<ShoppingCart> { let cart = await this.cartRepository.findOne({ userId }); if (!cart) { cart = await this.createNewCart(userId); } return this.calculateTotals(cart); }
async addItem(userId: string, productId: string, quantity: number): Promise<void> { const cart = await this.getCart(userId); const product = await this.productService.findById(productId);
const existingItem = cart.items.find(item => item.productId === productId); if (existingItem) { existingItem.quantity += quantity; } else { cart.items.push({ productId, quantity, price: product.price }); }
await this.cartRepository.save(this.calculateTotals(cart)); }
private calculateTotals(cart: ShoppingCart): ShoppingCart { cart.subtotal = cart.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); cart.tax = cart.subtotal * 0.1; // 10% tax cart.total = cart.subtotal + cart.tax; return cart; }
// ... other method implementations}Event System Interfaces
// Event system interfacesinterface Event { type: string; payload: unknown; timestamp: number;}
interface EventHandler<T> { handle(event: Event & { payload: T }): Promise<void>;}
interface EventBus { publish<T>(event: Omit<Event & { payload: T }, 'timestamp'>): Promise<void>; subscribe<T>(eventType: string, handler: EventHandler<T>): void; unsubscribe<T>(eventType: string, handler: EventHandler<T>): void;}
// Implementation exampleclass EventBusImpl implements EventBus { private handlers: Map<string, EventHandler<unknown>[]> = new Map();
async publish<T>(event: Omit<Event & { payload: T }, 'timestamp'>): Promise<void> { const handlers = this.handlers.get(event.type) || []; const fullEvent = { ...event, timestamp: Date.now() };
await Promise.all( handlers.map(handler => handler.handle(fullEvent)) ); }
subscribe<T>(eventType: string, handler: EventHandler<T>): void { const handlers = this.handlers.get(eventType) || []; handlers.push(handler as EventHandler<unknown>); this.handlers.set(eventType, handlers); }
unsubscribe<T>(eventType: string, handler: EventHandler<T>): void { const handlers = this.handlers.get(eventType) || []; this.handlers.set( eventType, handlers.filter(h => h !== handler) ); }}
// Usage exampleinterface UserCreatedEvent { userId: string; email: string;}
class EmailNotificationHandler implements EventHandler<UserCreatedEvent> { async handle(event: Event & { payload: UserCreatedEvent }): Promise<void> { const { userId, email } = event.payload; // Send welcome email console.log(`Sending welcome email to ${email}`); }}
// Using the event systemconst eventBus = new EventBusImpl();const emailHandler = new EmailNotificationHandler();
eventBus.subscribe<UserCreatedEvent>('user.created', emailHandler);
// Publishing an eventawait eventBus.publish<UserCreatedEvent>({ type: 'user.created', payload: { userId: '123', email: 'user@example.com' }});Best Practices
- Use Interface Segregation
// Good: Smaller, focused interfacesinterface Readable { read(): Buffer;}
interface Writable { write(data: Buffer): void;}
interface Closeable { close(): void;}
class FileStream implements Readable, Writable, Closeable { read(): Buffer { // Implementation return Buffer.from([]); }
write(data: Buffer): void { // Implementation }
close(): void { // Implementation }}
// Bad: Large, monolithic interfaceinterface FileOperations { read(): Buffer; write(data: Buffer): void; close(): void; // ... many more methods}- Use Generic Constraints
interface Repository<T extends { id: string }> { findById(id: string): Promise<T>; save(item: T): Promise<T>; delete(id: string): Promise<boolean>;}
// Now this interface can only be used with types that have an id propertyinterface User { id: string; name: string;}
class UserRepository implements Repository<User> { // Implementation}- Document Complex Interfaces
/** * Represents a configuration for the application. * @property apiKey - The API key for external service authentication * @property maxRetries - Maximum number of retry attempts for failed requests * @property timeout - Timeout in milliseconds for requests */interface ApplicationConfig { apiKey: string; maxRetries: number; timeout: number; endpoints: { auth: string; api: string; };}- Use Declaration Merging Wisely
// Original interfaceinterface Config { name: string;}
// Adding new properties through declaration merginginterface Config { version: string;}
// The resulting interface has both propertiesconst config: Config = { name: "MyApp", version: "1.0.0"};Conclusion
TypeScript interfaces are a powerful tool for defining contracts in your code. They provide:
- Clear type definitions for objects and functions
- Reusable type definitions across your codebase
- Support for optional and readonly properties
- Ability to describe complex object shapes and relationships
- Extension and implementation capabilities
Remember to:
- Keep interfaces focused and single-purpose
- Use interface segregation principle
- Document complex interfaces
- Use generics when appropriate
- Leverage declaration merging when needed
With these patterns and best practices, you can build more maintainable and type-safe TypeScript applications.