Skip to main content

Classes & OOP

TypeScript adds powerful features to JavaScript classes: access modifiers, abstract classes, and more. It makes object-oriented programming feel natural and type-safe.

1. Class Basics

Basic Class Structure

class User {
  // Properties
  name: string;
  email: string;

  // Constructor
  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }

  // Methods
  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

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

Parameter Properties (Shorthand)

TypeScript can automatically create and assign properties from constructor parameters:
class User {
  // public keyword creates and assigns the property
  constructor(
    public name: string,
    public email: string,
    public age: number = 0
  ) {}

  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

// Equivalent to the longer version above
const user = new User('Alice', '[email protected]', 25);

2. Access Modifiers

Control visibility of class members.

public (Default)

Accessible from anywhere.
class User {
  public name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const user = new User('Alice');
console.log(user.name); // ✅ Accessible

private

Only accessible within the class.
class BankAccount {
  private balance: number = 0;

  deposit(amount: number): void {
    this.balance += amount;
  }

  withdraw(amount: number): boolean {
    if (amount > this.balance) return false;
    this.balance -= amount;
    return true;
  }

  getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount();
account.deposit(100);
// account.balance; // ❌ Error: Property 'balance' is private
console.log(account.getBalance()); // 100

protected

Accessible within the class and subclasses.
class Animal {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  bark(): string {
    return `${this.name} says Woof!`; // ✅ Can access protected
  }
}

const dog = new Dog('Max');
// dog.name; // ❌ Error: Property 'name' is protected
console.log(dog.bark()); // "Max says Woof!"

Private Fields (ES2022)

True private fields using # syntax.
class Counter {
  #count = 0; // Truly private (runtime enforcement)

  increment(): number {
    return ++this.#count;
  }

  get value(): number {
    return this.#count;
  }
}

const counter = new Counter();
counter.increment();
// counter.#count; // ❌ Syntax error - truly inaccessible
console.log(counter.value); // 1
private vs #: private is TypeScript-only (compile-time). # is JavaScript-native (runtime-enforced). Use # for true encapsulation.

3. Readonly Properties

Properties that can only be set in the constructor.
class User {
  readonly id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  changeName(name: string): void {
    this.name = name; // ✅ OK
    // this.id = 999; // ❌ Error: Cannot assign to 'id' because it is read-only
  }
}

Combining with Parameter Properties

class User {
  constructor(
    public readonly id: number,
    public name: string
  ) {}
}

4. Getters and Setters

Define computed properties with validation.
class Circle {
  private _radius: number;

  constructor(radius: number) {
    this._radius = radius;
  }

  // Getter
  get radius(): number {
    return this._radius;
  }

  // Setter with validation
  set radius(value: number) {
    if (value <= 0) {
      throw new Error('Radius must be positive');
    }
    this._radius = value;
  }

  // Computed property (getter only)
  get area(): number {
    return Math.PI * this._radius ** 2;
  }

  get circumference(): number {
    return 2 * Math.PI * this._radius;
  }
}

const circle = new Circle(5);
console.log(circle.area);        // 78.54...
circle.radius = 10;              // Uses setter
console.log(circle.circumference); // 62.83...
// circle.area = 100;            // ❌ Error: Cannot set - no setter

5. Static Members

Properties and methods on the class itself, not instances.
class MathUtils {
  static PI = 3.14159;

  static add(a: number, b: number): number {
    return a + b;
  }

  static multiply(a: number, b: number): number {
    return a * b;
  }

  // Static block (TS 4.4+)
  static {
    console.log('MathUtils class initialized');
  }
}

// Access without instantiation
console.log(MathUtils.PI);           // 3.14159
console.log(MathUtils.add(2, 3));    // 5

// Cannot access on instance
const utils = new MathUtils();
// utils.PI; // ❌ Error

Singleton Pattern

class Database {
  private static instance: Database;
  private constructor() {} // Private constructor prevents new Database()

  static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }

  query(sql: string): void {
    console.log(`Executing: ${sql}`);
  }
}

const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true - same instance

6. Inheritance

Extend classes to create specialized versions.
class Animal {
  constructor(public name: string) {}

  move(distance: number): void {
    console.log(`${this.name} moved ${distance}m`);
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name); // Call parent constructor
  }

  bark(): void {
    console.log(`${this.name} says Woof!`);
  }

  // Override parent method
  move(distance: number): void {
    console.log('Running...');
    super.move(distance); // Call parent method
  }
}

const dog = new Dog('Max', 'Golden Retriever');
dog.bark();     // "Max says Woof!"
dog.move(10);   // "Running..." then "Max moved 10m"

Method Overriding with override

class Animal {
  speak(): void {
    console.log('Some sound');
  }
}

class Dog extends Animal {
  override speak(): void { // Explicitly marks override
    console.log('Woof!');
  }

  // override bark(): void {} // ❌ Error: no 'bark' in parent
}

7. Abstract Classes

Define templates for subclasses. Cannot be instantiated directly.
abstract class Shape {
  constructor(public color: string) {}

  // Abstract method - must be implemented by subclasses
  abstract getArea(): number;
  abstract getPerimeter(): number;

  // Concrete method - can be inherited
  describe(): string {
    return `A ${this.color} shape with area ${this.getArea()}`;
  }
}

class Circle extends Shape {
  constructor(color: string, public radius: number) {
    super(color);
  }

  getArea(): number {
    return Math.PI * this.radius ** 2;
  }

  getPerimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(
    color: string,
    public width: number,
    public height: number
  ) {
    super(color);
  }

  getArea(): number {
    return this.width * this.height;
  }

  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }
}

// const shape = new Shape('red'); // ❌ Error: Cannot create instance of abstract class

const circle = new Circle('red', 5);
console.log(circle.describe()); // "A red shape with area 78.54..."

const rect = new Rectangle('blue', 4, 6);
console.log(rect.getArea()); // 24

8. Implementing Interfaces

Classes can implement one or more interfaces.
interface Printable {
  print(): void;
}

interface Loggable {
  log(): void;
}

interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

class Document implements Printable, Loggable, Serializable {
  constructor(public content: string) {}

  print(): void {
    console.log(`Printing: ${this.content}`);
  }

  log(): void {
    console.log(`Logging: ${this.content}`);
  }

  serialize(): string {
    return JSON.stringify({ content: this.content });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.content = parsed.content;
  }
}

Interface vs Abstract Class

FeatureInterfaceAbstract Class
Multiple inheritance✅ Yes❌ No
Implementation code❌ No✅ Yes
Properties with values❌ No✅ Yes
Constructor❌ No✅ Yes
Access modifiers❌ No✅ Yes

9. Generic Classes

Classes that work with multiple types.
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }

  get size(): number {
    return this.items.length;
  }
}

// Type-safe stacks
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2

const stringStack = new Stack<string>();
stringStack.push('hello');
stringStack.push('world');
console.log(stringStack.peek()); // 'world'

Generic Constraints in Classes

interface Identifiable {
  id: number;
}

class Repository<T extends Identifiable> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  remove(id: number): boolean {
    const index = this.items.findIndex(item => item.id === id);
    if (index !== -1) {
      this.items.splice(index, 1);
      return true;
    }
    return false;
  }
}

interface User extends Identifiable {
  name: string;
}

const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: 'Alice' });
console.log(userRepo.findById(1)); // { id: 1, name: 'Alice' }

10. Decorators

Decorators are a stage 3 proposal for JavaScript. TypeScript has experimental support.
Enable decorators in tsconfig.json:
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Class Decorator

function Logger(constructor: Function) {
  console.log(`Creating instance of ${constructor.name}`);
}

@Logger
class User {
  constructor(public name: string) {}
}

const user = new User('Alice'); // Logs: "Creating instance of User"

Decorator Factory

function Logger(prefix: string) {
  return function (constructor: Function) {
    console.log(`${prefix}: ${constructor.name}`);
  };
}

@Logger('INFO')
class User {
  constructor(public name: string) {}
}

Method Decorator

function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`${propertyKey} returned: ${result}`);
    return result;
  };

  return descriptor;
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3);
// Logs: "Calling add with args: [2,3]"
// Logs: "add returned: 5"

Property Decorator

function Required(target: any, propertyKey: string) {
  let value: any;

  const getter = function () {
    return value;
  };

  const setter = function (newVal: any) {
    if (newVal === undefined || newVal === null || newVal === '') {
      throw new Error(`${propertyKey} is required`);
    }
    value = newVal;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class User {
  @Required
  name!: string;
}

const user = new User();
// user.name = ''; // ❌ Throws: "name is required"
user.name = 'Alice'; // ✅ OK

11. Practical Example: Service Layer

// Interfaces
interface Entity {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

interface UserEntity extends Entity {
  name: string;
  email: string;
  role: 'admin' | 'user';
}

// Base Repository
abstract class BaseRepository<T extends Entity> {
  protected items: T[] = [];

  abstract create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): T;

  findAll(): T[] {
    return [...this.items];
  }

  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  update(id: number, data: Partial<T>): T | undefined {
    const index = this.items.findIndex(item => item.id === id);
    if (index === -1) return undefined;

    this.items[index] = {
      ...this.items[index],
      ...data,
      updatedAt: new Date()
    };
    return this.items[index];
  }

  delete(id: number): boolean {
    const index = this.items.findIndex(item => item.id === id);
    if (index === -1) return false;
    this.items.splice(index, 1);
    return true;
  }
}

// User Repository
class UserRepository extends BaseRepository<UserEntity> {
  private nextId = 1;

  create(data: Omit<UserEntity, 'id' | 'createdAt' | 'updatedAt'>): UserEntity {
    const user: UserEntity = {
      ...data,
      id: this.nextId++,
      createdAt: new Date(),
      updatedAt: new Date()
    };
    this.items.push(user);
    return user;
  }

  findByEmail(email: string): UserEntity | undefined {
    return this.items.find(user => user.email === email);
  }

  findByRole(role: 'admin' | 'user'): UserEntity[] {
    return this.items.filter(user => user.role === role);
  }
}

// Usage
const userRepo = new UserRepository();

const alice = userRepo.create({
  name: 'Alice',
  email: '[email protected]',
  role: 'admin'
});

const bob = userRepo.create({
  name: 'Bob',
  email: '[email protected]',
  role: 'user'
});

console.log(userRepo.findByRole('admin')); // [alice]
console.log(userRepo.findByEmail('[email protected]')); // bob

Summary

ConceptExample
Class Definitionclass User { name: string }
Constructorconstructor(name: string) {}
Parameter Propertyconstructor(public name: string) {}
Access Modifierspublic, private, protected
Private Fields#privateField
Readonlyreadonly id: number
Getters/Settersget value(), set value(v)
Static Membersstatic PI = 3.14
Inheritanceclass Dog extends Animal
Abstract Classabstract class Shape
Implement Interfaceclass Doc implements Printable
Generic Classclass Stack<T>
Decorators@Logger class User
Next, we’ll dive deep into advanced types!