
How to Use Classes in TypeScript
TypeScript classes bridge the gap between object-oriented programming patterns and JavaScript’s prototype-based inheritance system, giving developers familiar OOP constructs while maintaining JavaScript’s flexibility. Understanding classes is crucial for building maintainable, scalable applications, especially when working with frameworks like Angular, NestJS, or enterprise-level Node.js projects. You’ll learn how to define classes, implement inheritance, use access modifiers, work with static members, and apply modern TypeScript features like decorators and generics to create robust, type-safe object structures.
How TypeScript Classes Work Under the Hood
TypeScript classes compile down to JavaScript functions with prototype-based inheritance, but they provide compile-time type checking and modern syntax sugar. When you define a class in TypeScript, the compiler generates equivalent JavaScript that works across different environments while preserving the object-oriented behavior you expect.
// 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 key difference from plain JavaScript is TypeScript’s compile-time type checking, access modifiers, and enhanced IntelliSense support. This means you catch errors during development rather than runtime, making your code more reliable and easier to maintain.
Basic Class Implementation Guide
Let’s start with a practical example building a simple task management system. This covers the fundamental concepts you’ll use in most TypeScript projects.
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('Cannot 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 use this class effectively:
// 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 work
// 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
TypeScript supports classical inheritance with the extends
keyword and abstract classes for defining contracts. This is particularly useful when building API frameworks or plugin systems.
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();
}
}
Advanced Features: Generics and Decorators
Generic classes allow you to create reusable components that work with multiple types, while decorators provide metadata and behavior modification capabilities.
// Generic Repository Pattern
interface IRepository<T> {
save(entity: T): Promise<T>;
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
delete(id: string): Promise<boolean>;
}
class InMemoryRepository<T extends BaseEntity> implements IRepository<T> {
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');
}
}
Real-World Use Cases and Patterns
Here are practical patterns you'll encounter in production applications, from API clients to state management systems.
// Singleton Pattern for Configuration Management
class AppConfig {
private static instance: AppConfig;
private config: Map<string, any>;
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<T>(path: string): Promise<T> {
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<string>('apiUrl');
const httpClient: HttpClient = new FetchHttpClient(apiUrl!);
TypeScript vs JavaScript Classes Comparison
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 | Not available | Type-safe reusable components |
Decorators | Experimental support | Stage 3 proposal | Metadata and AOP patterns |
Best Practices and Common Pitfalls
Follow these guidelines to write maintainable, performant TypeScript classes that scale well in team environments.
- Use composition over inheritance - Prefer injecting dependencies rather than deep inheritance hierarchies
- Implement interfaces explicitly - Use
implements
keyword to document your class contracts - Avoid any type in class definitions - Defeats the purpose of TypeScript's type safety
- Make properties readonly when possible - Prevents accidental mutations and improves predictability
- Use private constructors for singletons - Enforce single instance pattern properly
- Prefer dependency injection - Makes classes more testable and flexible
// Good: Composition with dependency injection
interface ILogger {
log(message: string): void;
}
interface IEmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
class UserService {
constructor(
private readonly logger: ILogger,
private readonly emailService: IEmailService,
private readonly userRepository: IRepository<User>
) {}
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<User> {
// 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 });
}
}
Common performance considerations include avoiding excessive getter/setter usage in hot paths, preferring readonly arrays over mutable ones, and using static methods when you don't need instance state.
// 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 no instance state 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 TypeScript class patterns and official documentation, check out the TypeScript Handbook on Classes and the TypeScript coding guidelines. These resources provide deeper insights into compiler options, advanced type patterns, and community best practices that will help you build robust applications.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.