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