How to Use Classes in TypeScript
Classes in TypeScript provide a bridge between object-oriented programming concepts and JavaScript’s prototype-based approach, offering developers the familiar constructs while retaining the language’s versatility. Grasping the concept of classes is essential for creating applications that are both maintainable and scalable, particularly when using frameworks such as Angular, NestJS, or large-scale de.js projects. You will discover how to declare classes, implement inheritance, utilise access modifiers, handle static properties, and leverage modern TypeScript features like decorators and generics to form robust, type-safe object models.
Understanding the Mechanics of TypeScript Classes
In TypeScript, classes transpile into JavaScript functions with prototype-based inheritance, yet they offer compile-time type validation and a more accessible syntax. Defining a class in TypeScript results in corresponding JavaScript that operates consistently across platforms while upholding expected object-oriented functionalities.
// TypeScript class class User { private id: number; public name: string;
constructor(id: number, name: string) { this.id = id; this.name = name; } public getId(): number { return this.id; }
}
// Compiled JavaScript (ES5 target)
var User = /* @class / (function () {
function User(id, name) {
this.id = id;
this.name = name;
}
User.prototype.getId = function () {
return this.id;
};
return User;
}());
The primary distinction from standard JavaScript lies in TypeScript’s compile-time type verification, access modifers, and enhanced IntelliSense capabilities. This ensures that errors are detected during development rather than at runtime, resulting in more reliable, easily maintainable code.
Guide to Basic Class Implementation
Let’s initiate a hands-on example by creating a straightforward task management system. This process covers the fundamental ideas you’ll engage with in most TypeScript applications.
enum TaskStatus { PENDING = 'pending', IN_PROGRESS = 'in_progress', COMPLETED = 'completed' }
class Task { // Property declarations with access modifiers private readonly id: string; public title: string; private _status: TaskStatus; private createdAt: Date;
// Static property for ID generation private static nextId: number = 1; constructor(title: string, status: TaskStatus = TaskStatus.PENDING) { this.id = `task_${Task.nextId++}`; this.title = title; this._status = status; this.createdAt = new Date(); } // Getter and setter for controlled access get status(): TaskStatus { return this._status; } set status(newStatus: TaskStatus) { if (this._status === TaskStatus.COMPLETED) { throw new Error('Cant change status of completed task'); } this._status = newStatus; } // Public methods public complete(): void { this._status = TaskStatus.COMPLETED; } public getInfo(): string { return `Task ${this.id}: ${this.title} (${this._status})`; } // Static method static createUrgentTask(title: string): Task { return new Task(`URGENT: ${title}`, TaskStatus.IN_PROGRESS); }
}
Here’s how to effectively utilise this class:
// Creating instances const task1 = new Task('Implement user authentication'); const urgentTask = Task.createUrgentTask('Fix production bug');
// Using getters and setters console.log(task1.status); // 'pending' task1.status = TaskStatus.IN_PROGRESS; // Valid // task1.status = TaskStatus.COMPLETED; // Would fail // task1.status = TaskStatus.PENDING; // Would throw error after completion
// Method calls task1.complete(); console.log(task1.getInfo()); // Task task_1: Implement user authentication (completed)
Inheritance and Abstract Classes in TypeScript
TypeScript accommodates classical inheritance with the
extends
keyword and enables defining contracts via abstract classes. This is particularly practical in developing API frameworks or plugin structures.abstract class BaseEntity { protected readonly id: string; protected createdAt: Date; protected updatedAt: Date;
constructor(id: string) { this.id = id; this.createdAt = new Date(); this.updatedAt = new Date(); } // Abstract method - must be implemented by subclasses abstract validate(): boolean; // Concrete method available to all subclasses protected touch(): void { this.updatedAt = new Date(); } public getId(): string { return this.id; }
}
class User extends BaseEntity {
private email: string;
private hashedPassword: string;constructor(id: string, email: string, password: string) { super(id); // Call parent constructor this.email = email; this.hashedPassword = this.hashPassword(password); } // Implementation of abstract method validate(): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(this.email) && this.hashedPassword.length > 0; } public updateEmail(newEmail: string): void { this.email = newEmail; this.touch(); // Call protected parent method } private hashPassword(password: string): string { // Simplified hashing - use bcrypt in production return Buffer.from(password).toString('base64'); }
}
class Product extends BaseEntity {
private name: string;
private price: number;constructor(id: string, name: string, price: number) { super(id); this.name = name; this.price = price; } validate(): boolean { return this.name.length > 0 && this.price > 0; } public updatePrice(newPrice: number): void { if (newPrice <= 0) throw new Error('Price must be positive'); this.price = newPrice; this.touch(); }
}
Exploring Advanced Features: Generics and Decorators
Generics enable the creation of versatile components that function with various types, while decorators allow for metadata addition and behaviour modification.
// Generic Repository Pattern interface IRepository
{ save(entity: T): Promise ; findById(id: string): Promise ; findAll(): Promise ; delete(id: string): Promise ; } class InMemoryRepository
implements IRepository { private items: Map<string, T> = new Map(); async save(entity: T): Promise<T> { if (!entity.validate()) { throw new Error('Entity validation failed'); } this.items.set(entity.getId(), entity); return entity; } async findById(id: string): Promise<T | null> { return this.items.get(id) || null; } async findAll(): Promise<T[]> { return Array.from(this.items.values()); } async delete(id: string): Promise<boolean> { return this.items.delete(id); } // Generic method with constraints findByPredicate<K extends keyof T>(key: K, value: T[K]): T[] { return Array.from(this.items.values()).filter(item => item[key] === value); }
}
// Decorator example (requires experimentalDecorators in tsconfig.json)
function LogMethod(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;descriptor.value = function(...args: any[]) { console.log(`Calling ${propertyName} with arguments:`, args); const result = method.apply(this, args); console.log(`Method ${propertyName} returned:`, result); return result; };
}
class ApiService {
@LogMethod
async fetchUser(id: string): Promise<User | null> {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 100));
return new User(id,user${id}@example.com
, 'password123');
}
}
Practical Applications and Patterns in the Real World
Here are methodologies you might encounter in real-life applications, ranging from API clients to state management frameworks.
// Singleton Pattern for Configuration Management class AppConfig { private static instance: AppConfig; private config: Map
; private constructor() { this.config = new Map(); this.loadDefaultConfig(); } static getInstance(): AppConfig { if (!AppConfig.instance) { AppConfig.instance = new AppConfig(); } return AppConfig.instance; } private loadDefaultConfig(): void { this.config.set('apiUrl', process.env.API_URL || 'http://localhost:3000'); this.config.set('timeout', parseInt(process.env.TIMEOUT || '5000')); this.config.set('retryAttempts', 3); } get<T>(key: string): T | undefined { return this.config.get(key); } set<T>(key: string, value: T): void { this.config.set(key, value); }
}
// Factory Pattern for HTTP Clients
abstract class HttpClient {
protected baseUrl: string;
protected timeout: number;constructor(baseUrl: string, timeout: number = 5000) { this.baseUrl = baseUrl; this.timeout = timeout; } abstract get<T>(path: string): Promise<T>; abstract post<T>(path: string, data: any): Promise<T>;
}
class AxiosHttpClient extends HttpClient {
private axios: any; // In real app, import axios typesconstructor(baseUrl: string, timeout?: number) { super(baseUrl, timeout); // this.axios = axios.create({ baseURL: baseUrl, timeout }); } async get<T>(path: string): Promise<T> { // return this.axios.get(path).then(response => response.data); throw new Error('Axios not implemented in this example'); } async post<T>(path: string, data: any): Promise<T> { // return this.axios.post(path, data).then(response => response.data); throw new Error('Axios not implemented in this example'); }
}
class FetchHttpClient extends HttpClient {
async get(path: string): Promise {
const response = await fetch(${this.baseUrl}${path}
, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } async post<T>(path: string, data: any): Promise<T> { const response = await fetch(`${this.baseUrl}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }
}
// Usage example
const config = AppConfig.getInstance();
const apiUrl = config.get('apiUrl');
const httpClient: HttpClient = new FetchHttpClient(apiUrl!);
Comparing TypeScript Classes with JavaScript
Feature | TypeScript | JavaScript (ES6+) | Benefits |
---|---|---|---|
Type Safety | Compile-time type checking | Runtime type checking only | Catch errors early, better IDE support |
Access Modifiers | private, protected, public, readonly | # for private (recent) | Clear API boundaries, encapsulation |
Abstract Classes | Full support with abstract keyword | Convention-based only | Enforce implementation contracts |
Interfaces | Interface implementation | Duck typing only | Design by contract, documentation |
Generics | Built-in generic support | No direct support | Type-safe reusable components |
Decorators | Experimental support | Stage 3 proposal | Metadata and AOP patterns |
Best Practices and Common Pitfalls in TypeScript
Adhere to these recommendations to craft maintainable, high-performing TypeScript classes that are suitable for team environments.
- Prefer composition over inheritance: Inject dependencies instead of creating deep inheritance trees.
- Explicitly implement interfaces: Use the
implements
keyword for clear class contracts. - Avoid the ‘any’ type in class definitions: This undermines TypeScript’s type safety.
- Make properties readonly where feasible: Helps avert accidental changes and enhances predictability.
- Use private constructors for singletons: Ensures the correct implementation of the singleton pattern.
- Favour dependency injection: Improves the testability and adaptability of classes.
// Good: Composition with dependency injection interface ILogger { log(message: string): void; }
interface IEmailService { send(to: string, subject: string, body: string): Promise
; } class UserService { constructor( private readonly logger: ILogger, private readonly emailService: IEmailService, private readonly userRepository: IRepository
) {} async createUser(email: string, password: string): Promise<User> { this.logger.log(`Creating user with email: ${email}`); const user = new User(crypto.randomUUID(), email, password); await this.userRepository.save(user); await this.emailService.send( email, 'Welcome!', 'Your account has been created successfully.' ); return user; }
}
// Bad: Tight coupling and inheritance abuse
class UserServiceBad extends BaseService {
async createUser(email: string, password: string): Promise{
// Direct dependencies - hard to test
console.log(Creating user: ${email}
);
const emailer = new EmailService();
const db = new DatabaseConnection();// Difficult to mock or replace return db.users.create({ email, password }); }
}
Consider performance by minimising excessive getter/setter usage in critical areas, opting for readonly arrays over mutable ones, and employing static methods where instance state is unnecessary.
// Performance-conscious class design class DataProcessor { // Use readonly for arrays that shouldn't change private readonly processors: ReadonlyArray<(data: any) => any>;
constructor(processors: Array<(data: any) => any>) { this.processors = Object.freeze([...processors]); } // Static method when instance state isn't needed static validateInput(data: any): boolean { return data != null && typeof data === 'object'; } // Avoid getters in loops - cache the result instead private _processedCount: number = 0; get processedCount(): number { return this._processedCount; } processData(items: any[]): any[] { if (!DataProcessor.validateInput(items)) { throw new Error('Invalid input data'); } const results = []; for (const item of items) { let processed = item; for (const processor of this.processors) { processed = processor(processed); } results.push(processed); this._processedCount++; // Update count once per item } return results; }
}
For more advanced class patterns with TypeScript and official documentation, visit the TypeScript Handbook on Classes and the TypeScript coding guidelines. These resources provide comprehensive insights into compiler options, complex type patterns, and community best practices to assist you in crafting resilient applications.
This article integrates insights and content from various online resources. We acknowledge and value the contributions of all original authors, publishers, and websites. Efforts have been made to credit source material appropriately; any unintentional oversights do not constitute copyright infringement. All trademarks, logos, and images belong to their respective owners. Should you believe any content used here infringes upon your copyright, please reach out to us for a prompt review and action.
This article is meant solely for informational and educational purposes and does not infringe on any copyright owners’ rights. If any copyrighted material has been used without correct attribution or in violation of copyright laws, this is unintentional, and we will rectify it promptly upon notification. Note that the redistribution, republication, or reproduction of any content in this article, wholly or partially, is not permitted without explicit written consent from the author and website owner. For permissions or further inquiries, please contact us.