Classes and Object-Oriented Programming

1. Class Declaration and Constructor Functions

Feature Syntax Description Use Case
Class Declaration class Name { } Define blueprint for objects with properties and methods OOP patterns, encapsulation
Constructor constructor(args) { } Initialize instance when object created with new Object initialization, DI
Class Expression const C = class { } Anonymous or named class assigned to variable Factory patterns, conditional classes
Generic Class class Name<T> { } Class with type parameters for reusability Collections, containers

Example: Basic class declaration

// Basic class
class User {
    id: number;
    name: string;
    
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
    
    greet(): string {
        return `Hello, I'm ${this.name}`;
    }
}

const user = new User(1, "Alice");
console.log(user.greet());  // "Hello, I'm Alice"

// Class with optional constructor parameters
class Product {
    constructor(
        public name: string,
        public price: number,
        public discount?: number
    ) {}
    
    getFinalPrice(): number {
        return this.discount 
            ? this.price * (1 - this.discount)
            : this.price;
    }
}

// Class expression
const Animal = class {
    constructor(public name: string) {}
    
    makeSound(): void {
        console.log("Some sound");
    }
};

Example: Generic classes

// Generic class
class Box<T> {
    private value: T;
    
    constructor(value: T) {
        this.value = value;
    }
    
    getValue(): T {
        return this.value;
    }
    
    setValue(value: T): void {
        this.value = value;
    }
}

const numberBox = new Box<number>(42);
const stringBox = new Box("hello");  // Type inferred

// Generic class with constraints
class Collection<T extends { id: string }> {
    private items: T[] = [];
    
    add(item: T): void {
        this.items.push(item);
    }
    
    findById(id: string): T | undefined {
        return this.items.find(item => item.id === id);
    }
}

2. Property Declaration and Initialization

Pattern Syntax Description Initialization
Declared Property prop: Type; Property with type annotation - must be initialized Constructor or inline
Initialized Property prop: Type = value; Property with default value Inline initialization
Optional Property prop?: Type; May be undefined - no initialization required Optional
Definite Assignment prop!: Type; Assert property will be initialized - skip strict check External/async

Example: Property initialization patterns

// Various initialization patterns
class User {
    // Inline initialization
    id: number = 0;
    
    // Declared, initialized in constructor
    name: string;
    
    // Default value
    role: string = "user";
    
    // Optional property
    email?: string;
    
    // Definite assignment assertion
    token!: string;  // Will be set by external method
    
    constructor(name: string) {
        this.name = name;
    }
    
    setToken(token: string): void {
        this.token = token;
    }
}

// Complex initialization
class Config {
    // Readonly after initialization
    readonly apiUrl: string;
    
    // Computed property
    timestamp: Date = new Date();
    
    // Array initialization
    tags: string[] = [];
    
    // Object initialization
    metadata: Record<string, any> = {};
    
    constructor(apiUrl: string) {
        this.apiUrl = apiUrl;
    }
}

3. Access Modifiers

Modifier Visible To Description Use Case
public Everywhere Default - accessible from anywhere Public API, external access
private Same class only Only accessible within declaring class Implementation details, encapsulation
protected Class + subclasses Accessible in class and derived classes Inheritance, template methods
readonly Immutable after init Cannot be reassigned after initialization Constants, configuration
#private ES2022 Runtime private True runtime privacy via JavaScript private fields True encapsulation

Example: Access modifiers

class BankAccount {
    // Public - accessible everywhere
    public accountNumber: string;
    
    // Private - only within class
    private balance: number;
    
    // Protected - class and subclasses
    protected owner: string;
    
    // Readonly - cannot change after initialization
    readonly createdAt: Date;
    
    // ES2022 private field (runtime private)
    #pin: string;
    
    constructor(accountNumber: string, owner: string, pin: string) {
        this.accountNumber = accountNumber;
        this.balance = 0;
        this.owner = owner;
        this.createdAt = new Date();
        this.#pin = pin;
    }
    
    // Public method
    public deposit(amount: number): void {
        this.balance += amount;  // OK - private accessible here
    }
    
    // Private method
    private validatePin(pin: string): boolean {
        return this.#pin === pin;
    }
    
    // Public method using private method
    public withdraw(amount: number, pin: string): boolean {
        if (!this.validatePin(pin)) {
            return false;
        }
        if (this.balance >= amount) {
            this.balance -= amount;
            return true;
        }
        return false;
    }
    
    // Public getter for private property
    public getBalance(): number {
        return this.balance;
    }
}

const account = new BankAccount("123456", "Alice", "1234");
account.deposit(100);           // OK - public
console.log(account.accountNumber);  // OK - public
// console.log(account.balance);     // Error - private
// console.log(account.#pin);        // Error - private field

Example: Protected members in inheritance

class Employee {
    protected employeeId: string;
    private salary: number;
    
    constructor(id: string, salary: number) {
        this.employeeId = id;
        this.salary = salary;
    }
    
    protected getSalary(): number {
        return this.salary;
    }
}

class Manager extends Employee {
    constructor(id: string, salary: number, private department: string) {
        super(id, salary);
    }
    
    // Can access protected members
    getDetails(): string {
        return `ID: ${this.employeeId}, Dept: ${this.department}`;
    }
    
    // Can call protected methods
    getSalaryWithBonus(): number {
        return this.getSalary() * 1.2;
    }
    
    // Cannot access private members
    // getBadSalary(): number {
    //     return this.salary;  // Error - private to Employee
    // }
}

const manager = new Manager("M001", 80000, "IT");
console.log(manager.getDetails());  // OK
// console.log(manager.employeeId);  // Error - protected

4. Static Members and Static Initialization Blocks

Feature Syntax Description Use Case
Static Property static prop: Type Class-level property - shared across all instances Constants, counters, config
Static Method static method() { } Called on class, not instance - no access to this Utilities, factories, helpers
Static Block TS 4.4+ static { } Initialize static properties with complex logic Static initialization, setup
Static Access Modifiers private static Combine static with access modifiers Internal static helpers

Example: Static members

class User {
    // Static property - shared across instances
    static totalUsers: number = 0;
    
    // Static readonly
    static readonly MAX_USERS: number = 1000;
    
    // Private static
    private static nextId: number = 1;
    
    id: number;
    name: string;
    
    constructor(name: string) {
        this.id = User.nextId++;
        this.name = name;
        User.totalUsers++;
    }
    
    // Static method
    static getUserCount(): number {
        return User.totalUsers;
    }
    
    // Static factory method
    static createAdmin(name: string): User {
        const user = new User(name);
        // Additional admin setup
        return user;
    }
    
    // Static utility
    static isValidEmail(email: string): boolean {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    }
}

// Access static members on class
console.log(User.totalUsers);        // 0
console.log(User.MAX_USERS);         // 1000
console.log(User.getUserCount());    // 0

const user1 = new User("Alice");
const user2 = new User("Bob");

console.log(User.totalUsers);        // 2
console.log(User.isValidEmail("test@email.com"));  // true

Example: Static initialization blocks

class Config {
    static apiUrl: string;
    static timeout: number;
    static features: Map<string, boolean>;
    
    // Static initialization block
    static {
        // Complex initialization logic
        const env = process.env.NODE_ENV || "development";
        
        if (env === "production") {
            Config.apiUrl = "https://api.production.com";
            Config.timeout = 5000;
        } else {
            Config.apiUrl = "https://api.dev.com";
            Config.timeout = 30000;
        }
        
        // Initialize complex structures
        Config.features = new Map();
        Config.features.set("darkMode", true);
        Config.features.set("notifications", false);
        
        console.log("Config initialized for", env);
    }
    
    static getFeature(name: string): boolean {
        return Config.features.get(name) ?? false;
    }
}

// Static block runs when class first evaluated
console.log(Config.apiUrl);  // URL based on environment

5. Abstract Classes and Abstract Methods

Feature Syntax Description Use Case
Abstract Class abstract class Name Cannot be instantiated - must be extended Base classes, templates
Abstract Method abstract method(): Type No implementation - must be implemented by subclass Contract enforcement, polymorphism
Abstract Property abstract prop: Type Property declaration without implementation Required properties in subclasses
Concrete Methods Regular methods in abstract class Provide default implementation - can be overridden Shared behavior, template methods

Example: Abstract classes

// Abstract base class
abstract class Shape {
    // Abstract property
    abstract name: string;
    
    // Concrete property
    color: string = "black";
    
    // Abstract methods - must be implemented
    abstract getArea(): number;
    abstract getPerimeter(): number;
    
    // Concrete method - shared implementation
    describe(): string {
        return `${this.name} with area ${this.getArea()}`;
    }
    
    // Concrete method using abstract methods (template method pattern)
    display(): void {
        console.log(`${this.name}:`);
        console.log(`  Area: ${this.getArea()}`);
        console.log(`  Perimeter: ${this.getPerimeter()}`);
    }
}

// Cannot instantiate abstract class
// const shape = new Shape();  // Error!

// Concrete implementation
class Circle extends Shape {
    name = "Circle";
    
    constructor(public radius: number) {
        super();
    }
    
    getArea(): number {
        return Math.PI * this.radius ** 2;
    }
    
    getPerimeter(): number {
        return 2 * Math.PI * this.radius;
    }
}

class Rectangle extends Shape {
    name = "Rectangle";
    
    constructor(public width: number, public height: number) {
        super();
    }
    
    getArea(): number {
        return this.width * this.height;
    }
    
    getPerimeter(): number {
        return 2 * (this.width + this.height);
    }
}

const circle = new Circle(5);
circle.display();  // Uses inherited display() method

const rect = new Rectangle(4, 6);
console.log(rect.describe());  // "Rectangle with area 24"

6. Class Inheritance and super Keyword

Feature Syntax Description Use Case
Inheritance class B extends A Derive class from parent - inherit properties/methods Code reuse, specialization
super() Call super(args) Call parent constructor - required in derived constructor Initialize parent class
super.method() super.methodName() Call parent method from overridden method Extend parent behavior
Method Override Same signature as parent Replace parent method implementation Polymorphism, specialization

Example: Class inheritance

// Base class
class Animal {
    constructor(public name: string, public age: number) {}
    
    makeSound(): void {
        console.log("Some generic sound");
    }
    
    describe(): string {
        return `${this.name} is ${this.age} years old`;
    }
}

// Derived class
class Dog extends Animal {
    constructor(name: string, age: number, public breed: string) {
        super(name, age);  // Must call parent constructor
    }
    
    // Override parent method
    makeSound(): void {
        console.log("Woof! Woof!");
    }
    
    // New method specific to Dog
    fetch(): void {
        console.log(`${this.name} is fetching`);
    }
    
    // Override and extend parent method
    describe(): string {
        const baseDescription = super.describe();  // Call parent
        return `${baseDescription} and is a ${this.breed}`;
    }
}

const dog = new Dog("Buddy", 3, "Golden Retriever");
dog.makeSound();      // "Woof! Woof!" (overridden)
dog.fetch();          // "Buddy is fetching" (new method)
console.log(dog.describe());  // Uses both parent and child logic

Example: Inheritance chain

// Multi-level inheritance
class Entity {
    constructor(public id: string) {}
}

class TimestampedEntity extends Entity {
    createdAt: Date;
    updatedAt: Date;
    
    constructor(id: string) {
        super(id);
        this.createdAt = new Date();
        this.updatedAt = new Date();
    }
    
    touch(): void {
        this.updatedAt = new Date();
    }
}

class User extends TimestampedEntity {
    constructor(id: string, public name: string, public email: string) {
        super(id);
    }
    
    updateProfile(name: string, email: string): void {
        this.name = name;
        this.email = email;
        this.touch();  // Inherited from TimestampedEntity
    }
}

const user = new User("u1", "Alice", "alice@email.com");
// Has properties from all levels: id, createdAt, updatedAt, name, email

7. Getters and Setters with Type Safety

Feature Syntax Description Use Case
Getter get prop(): Type Computed property - executed when accessed Derived values, validation
Setter set prop(value: Type) Intercept property assignment - validate/transform Validation, side effects
Readonly via Getter Getter without setter Property that can be read but not set externally Computed readonly properties
Access Modifiers private get/set Control visibility of accessors Encapsulation

Example: Getters and setters

class User {
    private _email: string = "";
    private _age: number = 0;
    
    // Getter
    get email(): string {
        return this._email;
    }
    
    // Setter with validation
    set email(value: string) {
        if (!value.includes("@")) {
            throw new Error("Invalid email format");
        }
        this._email = value;
    }
    
    // Getter with transformation
    get age(): number {
        return this._age;
    }
    
    // Setter with validation
    set age(value: number) {
        if (value < 0 || value > 150) {
            throw new Error("Invalid age");
        }
        this._age = value;
    }
    
    // Readonly computed property (getter only)
    get isAdult(): boolean {
        return this._age >= 18;
    }
}

const user = new User();
user.email = "alice@email.com";  // Calls setter
console.log(user.email);          // Calls getter
console.log(user.isAdult);        // Computed property
// user.isAdult = true;           // Error: no setter

Example: Advanced accessor patterns

class Rectangle {
    constructor(private _width: number, private _height: number) {}
    
    // Width accessor
    get width(): number {
        return this._width;
    }
    
    set width(value: number) {
        if (value <= 0) throw new Error("Width must be positive");
        this._width = value;
    }
    
    // Height accessor
    get height(): number {
        return this._height;
    }
    
    set height(value: number) {
        if (value <= 0) throw new Error("Height must be positive");
        this._height = value;
    }
    
    // Computed properties
    get area(): number {
        return this._width * this._height;
    }
    
    get perimeter(): number {
        return 2 * (this._width + this._height);
    }
    
    // Setter that affects multiple properties
    set dimensions(value: { width: number; height: number }) {
        this.width = value.width;
        this.height = value.height;
    }
}

const rect = new Rectangle(10, 5);
console.log(rect.area);       // 50
rect.width = 20;
console.log(rect.area);       // 100
rect.dimensions = { width: 15, height: 10 };
console.log(rect.perimeter);  // 50

8. Parameter Properties and Shorthand Syntax

Feature Syntax Description Benefit
Parameter Property constructor(public prop) Declare and initialize property in constructor parameter Reduced boilerplate
Public Parameter public name: Type Creates public property automatically Concise syntax
Private Parameter private name: Type Creates private property automatically Encapsulation shorthand
Protected Parameter protected name: Type Creates protected property automatically Inheritance shorthand
Readonly Parameter readonly name: Type Creates readonly property automatically Immutability shorthand

Example: Parameter properties (before/after)

// Traditional verbose syntax
class User {
    id: number;
    name: string;
    email: string;
    
    constructor(id: number, name: string, email: string) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

// Parameter properties - concise equivalent
class UserShort {
    constructor(
        public id: number,
        public name: string,
        public email: string
    ) {}
}

// Both create identical classes!
const user1 = new User(1, "Alice", "alice@email.com");
const user2 = new UserShort(1, "Alice", "alice@email.com");

// Mixed: parameter properties + regular properties
class Product {
    discount: number = 0;  // Regular property
    
    constructor(
        public id: string,
        public name: string,
        private _price: number
    ) {}
    
    get price(): number {
        return this._price * (1 - this.discount);
    }
}

Example: Parameter properties with modifiers

class BankAccount {
    constructor(
        readonly accountNumber: string,      // Readonly
        private balance: number,              // Private
        protected owner: string,              // Protected
        public bankName: string               // Public
    ) {}
    
    deposit(amount: number): void {
        this.balance += amount;  // Can modify private property
    }
    
    getBalance(): number {
        return this.balance;
    }
}

const account = new BankAccount("12345", 1000, "Alice", "MyBank");
console.log(account.accountNumber);  // OK - readonly
console.log(account.bankName);       // OK - public
// console.log(account.balance);     // Error - private
// console.log(account.owner);       // Error - protected
// account.accountNumber = "99999";  // Error - readonly

// Readonly parameter properties for immutable data
class Point {
    constructor(
        readonly x: number,
        readonly y: number
    ) {}
    
    distanceFromOrigin(): number {
        return Math.sqrt(this.x ** 2 + this.y ** 2);
    }
}

const point = new Point(3, 4);
console.log(point.x, point.y);        // 3, 4
// point.x = 5;                       // Error - readonly

OOP Best Practices

  • Use parameter properties to reduce boilerplate
  • Apply private to implementation details for encapsulation
  • Use protected for properties needed in inheritance
  • Leverage abstract classes for template methods
  • Prefer getters/setters for computed or validated properties
  • Use static for utilities and factory methods
  • Mark immutable properties as readonly
  • Always call super() first in derived class constructors