Loading Now

How to Use Classes in TypeScript

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 types

constructor(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.