You are viewing a preview of this lesson. Sign in to start learning
Back to TypeScript

Object-Oriented TypeScript

Implement OOP concepts including classes, inheritance, polymorphism, and access modifiers for structured code architecture.

Object-Oriented TypeScript

Master object-oriented programming in TypeScript with free flashcards and hands-on coding examples. This lesson covers classes, interfaces, inheritance, access modifiers, abstract classes, and advanced OOP patternsβ€”essential concepts for building scalable, maintainable TypeScript applications.

πŸ’» Welcome to Object-Oriented TypeScript

Object-Oriented Programming (OOP) is a paradigm that organizes code around objects and classes rather than functions and logic. TypeScript enhances JavaScript's OOP capabilities with static typing, interfaces, and access modifiers, making your code more robust and self-documenting.

Whether you're building a web application, server-side API, or complex enterprise system, understanding OOP in TypeScript will help you write cleaner, more maintainable code. Let's dive into the core concepts that make TypeScript's OOP features so powerful! πŸš€


πŸ—οΈ Core Concepts

1️⃣ Classes and Objects

A class is a blueprint for creating objects. It encapsulates data (properties) and behavior (methods) into a single unit.

class Person {
  name: string;
  age: number;

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

  greet(): string {
    return `Hello, my name is ${this.name}`;
  }
}

const person = new Person("Alice", 30);
console.log(person.greet()); // "Hello, my name is Alice"

Key Components:

  • Properties: name and age store data
  • Constructor: Special method that runs when creating a new instance
  • Methods: Functions that define behavior (greet)
  • Instance: person is an object created from the Person class

πŸ’‘ Tip: TypeScript requires you to declare property types, unlike JavaScript. This catches errors at compile-time!

2️⃣ Access Modifiers

TypeScript provides three access modifiers to control visibility:

Modifier Accessible From Use Case
public Anywhere (default) Public API, external access
private Only within the class Internal implementation details
protected Class and subclasses Shared with child classes
class BankAccount {
  public accountNumber: string;
  private balance: number;
  protected owner: string;

  constructor(accountNumber: string, owner: string) {
    this.accountNumber = accountNumber;
    this.owner = owner;
    this.balance = 0;
  }

  public deposit(amount: number): void {
    this.balance += amount; // Can access private property internally
  }

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

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

🧠 Mnemonic: PPP - Public for everyone, Private for me, Protected for family (subclasses).

3️⃣ Interfaces

An interface defines a contract that classes must follow. It specifies what properties and methods a class must implement without providing implementation details.

interface Vehicle {
  brand: string;
  speed: number;
  accelerate(): void;
  brake(): void;
}

class Car implements Vehicle {
  brand: string;
  speed: number;

  constructor(brand: string) {
    this.brand = brand;
    this.speed = 0;
  }

  accelerate(): void {
    this.speed += 10;
  }

  brake(): void {
    this.speed = Math.max(0, this.speed - 10);
  }
}

const myCar = new Car("Toyota");
myCar.accelerate();
console.log(myCar.speed); // 10

Interfaces vs. Classes:

  • Interfaces: Define structure only (no implementation)
  • Classes: Provide both structure and implementation
  • A class can implement multiple interfaces but extend only one class

πŸ’‘ Tip: Use interfaces for type checking and contracts. Use classes when you need actual object instances.

4️⃣ Inheritance and Extending Classes

Inheritance allows a class to inherit properties and methods from a parent class, promoting code reuse.

class Animal {
  protected name: string;

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

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

class Dog extends Animal {
  bark(): void {
    console.log(`${this.name} barks: Woof! Woof!`);
  }
}

class Bird extends Animal {
  fly(distance: number): void {
    console.log(`${this.name} flew ${distance} meters`);
  }
}

const dog = new Dog("Buddy");
dog.move(10);  // "Buddy moved 10 meters"
dog.bark();    // "Buddy barks: Woof! Woof!"

const bird = new Bird("Tweety");
bird.move(5);  // "Tweety moved 5 meters"
bird.fly(20);  // "Tweety flew 20 meters"

Key Concepts:

  • extends: Keyword to inherit from a parent class
  • super: Keyword to call parent class constructor or methods
  • Method Overriding: Child classes can override parent methods

5️⃣ Abstract Classes

Abstract classes cannot be instantiated directly. They serve as base classes that define common behavior for subclasses.

abstract class Shape {
  abstract getArea(): number;  // Must be implemented by subclasses
  
  describe(): string {
    return `This shape has an area of ${this.getArea()}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

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

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

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

const circle = new Circle(5);
console.log(circle.describe()); // "This shape has an area of 78.54..."

const rect = new Rectangle(4, 6);
console.log(rect.describe()); // "This shape has an area of 24"

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

Abstract vs. Interface:

  • Abstract classes can have implementation (concrete methods)
  • Interfaces have no implementation (pure contracts)
  • A class can implement multiple interfaces but extend only one abstract class

6️⃣ Property Shorthand in Constructors

TypeScript provides a shorthand for declaring and initializing properties directly in the constructor:

// Long way
class Product {
  name: string;
  price: number;

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

// Shorthand way
class Product {
  constructor(public name: string, public price: number) {
    // Properties automatically declared and initialized!
  }
}

const item = new Product("Laptop", 999);
console.log(item.name);  // "Laptop"
console.log(item.price); // 999

πŸ’‘ Tip: This shorthand works with public, private, protected, and readonly modifiers.

7️⃣ Getters and Setters

Getters and setters allow you to control how properties are accessed and modified:

class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("Temperature below absolute zero!");
    }
    this._celsius = value;
  }

  get fahrenheit(): number {
    return (this._celsius * 9/5) + 32;
  }

  set fahrenheit(value: number) {
    this._celsius = (value - 32) * 5/9;
  }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77

temp.fahrenheit = 86;
console.log(temp.celsius); // 30

πŸ”§ Try this: Add validation in setters to ensure data integrity (e.g., non-negative prices, valid email formats).

8️⃣ Static Members

Static properties and methods belong to the class itself, not to instances:

class MathHelper {
  static PI: number = 3.14159;

  static calculateCircleArea(radius: number): number {
    return this.PI * radius ** 2;
  }
}

// Call without creating an instance
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.calculateCircleArea(5)); // 78.5398

Use Cases:

  • Utility functions (e.g., Math.random(), Array.from())
  • Constants shared across all instances
  • Factory methods for creating instances

πŸ“ Practical Examples

Example 1: E-Commerce System

Let's build a simple e-commerce system with products and shopping carts:

interface Priceable {
  getPrice(): number;
}

class Product implements Priceable {
  constructor(
    public id: string,
    public name: string,
    private price: number,
    public stock: number
  ) {}

  getPrice(): number {
    return this.price;
  }

  reduceStock(quantity: number): boolean {
    if (this.stock >= quantity) {
      this.stock -= quantity;
      return true;
    }
    return false;
  }
}

class DiscountedProduct extends Product {
  constructor(
    id: string,
    name: string,
    price: number,
    stock: number,
    private discountPercent: number
  ) {
    super(id, name, price, stock);
  }

  getPrice(): number {
    const originalPrice = super.getPrice();
    return originalPrice * (1 - this.discountPercent / 100);
  }
}

class ShoppingCart {
  private items: Map<Product, number> = new Map();

  addItem(product: Product, quantity: number): void {
    const currentQty = this.items.get(product) || 0;
    this.items.set(product, currentQty + quantity);
  }

  getTotalPrice(): number {
    let total = 0;
    this.items.forEach((quantity, product) => {
      total += product.getPrice() * quantity;
    });
    return total;
  }

  checkout(): boolean {
    for (const [product, quantity] of this.items) {
      if (!product.reduceStock(quantity)) {
        return false; // Not enough stock
      }
    }
    this.items.clear();
    return true;
  }
}

// Usage
const laptop = new Product("L001", "Laptop", 999, 10);
const phone = new DiscountedProduct("P001", "Phone", 699, 20, 15);

const cart = new ShoppingCart();
cart.addItem(laptop, 1);
cart.addItem(phone, 2);

console.log(`Total: $${cart.getTotalPrice()}`); // Total: $2187.30
console.log(cart.checkout()); // true

Key Takeaways:

  • Interface Priceable ensures all products have a getPrice() method
  • Inheritance allows DiscountedProduct to override pricing logic
  • Encapsulation keeps stock management internal to Product
  • Polymorphism lets ShoppingCart work with any Priceable item

Example 2: User Authentication System

abstract class User {
  constructor(
    protected username: string,
    private password: string
  ) {}

  authenticate(inputPassword: string): boolean {
    return this.password === inputPassword;
  }

  abstract getPermissions(): string[];
}

class AdminUser extends User {
  getPermissions(): string[] {
    return ["read", "write", "delete", "manage_users"];
  }
}

class RegularUser extends User {
  getPermissions(): string[] {
    return ["read", "write"];
  }
}

class GuestUser extends User {
  getPermissions(): string[] {
    return ["read"];
  }
}

class AuthenticationService {
  private users: User[] = [];

  registerUser(user: User): void {
    this.users.push(user);
  }

  login(username: string, password: string): User | null {
    const user = this.users.find(u => u['username'] === username);
    if (user && user.authenticate(password)) {
      return user;
    }
    return null;
  }
}

// Usage
const authService = new AuthenticationService();
const admin = new AdminUser("admin", "admin123");
const user = new RegularUser("john", "pass456");

authService.registerUser(admin);
authService.registerUser(user);

const loggedIn = authService.login("john", "pass456");
if (loggedIn) {
  console.log(loggedIn.getPermissions()); // ["read", "write"]
}

Design Patterns Applied:

  • Template Method Pattern: Abstract User class defines authentication, subclasses define permissions
  • Polymorphism: AuthenticationService works with any User subclass
  • Encapsulation: Password is private, accessible only through authenticate()

Example 3: Game Character System

interface Movable {
  move(x: number, y: number): void;
}

interface Attackable {
  attack(target: Character): void;
}

abstract class Character implements Movable {
  protected x: number = 0;
  protected y: number = 0;

  constructor(
    public name: string,
    protected health: number,
    protected attackPower: number
  ) {}

  move(x: number, y: number): void {
    this.x = x;
    this.y = y;
    console.log(`${this.name} moved to (${x}, ${y})`);
  }

  takeDamage(amount: number): void {
    this.health = Math.max(0, this.health - amount);
    if (this.health === 0) {
      console.log(`${this.name} has been defeated!`);
    }
  }

  isAlive(): boolean {
    return this.health > 0;
  }

  abstract specialAbility(): void;
}

class Warrior extends Character implements Attackable {
  attack(target: Character): void {
    console.log(`${this.name} attacks ${target.name}!`);
    target.takeDamage(this.attackPower);
  }

  specialAbility(): void {
    console.log(`${this.name} uses Shield Bash!`);
    this.attackPower *= 1.5;
  }
}

class Mage extends Character implements Attackable {
  private mana: number = 100;

  attack(target: Character): void {
    if (this.mana >= 10) {
      console.log(`${this.name} casts Fireball at ${target.name}!`);
      target.takeDamage(this.attackPower * 2);
      this.mana -= 10;
    } else {
      console.log(`${this.name} is out of mana!`);
    }
  }

  specialAbility(): void {
    console.log(`${this.name} meditates and restores mana`);
    this.mana = 100;
  }
}

// Usage
const warrior = new Warrior("Conan", 100, 20);
const mage = new Mage("Gandalf", 80, 15);

warrior.move(10, 5);
mage.move(12, 6);

warrior.attack(mage);  // Conan attacks Gandalf!
mage.attack(warrior);  // Gandalf casts Fireball at Conan!

warrior.specialAbility();
mage.specialAbility();

OOP Principles Demonstrated:

  • Abstraction: Character hides implementation details
  • Inheritance: Warrior and Mage share common character traits
  • Polymorphism: Both implement Attackable but with different behavior
  • Encapsulation: Health and mana are protected/private

Example 4: Event System with Observers

interface Observer {
  update(message: string): void;
}

class Subject {
  private observers: Observer[] = [];

  attach(observer: Observer): void {
    this.observers.push(observer);
  }

  detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(message: string): void {
    for (const observer of this.observers) {
      observer.update(message);
    }
  }
}

class EmailNotifier implements Observer {
  constructor(private email: string) {}

  update(message: string): void {
    console.log(`Email to ${this.email}: ${message}`);
  }
}

class SMSNotifier implements Observer {
  constructor(private phoneNumber: string) {}

  update(message: string): void {
    console.log(`SMS to ${this.phoneNumber}: ${message}`);
  }
}

class OrderSystem extends Subject {
  placeOrder(orderId: string): void {
    console.log(`Order ${orderId} placed`);
    this.notify(`Your order ${orderId} has been confirmed`);
  }

  shipOrder(orderId: string): void {
    console.log(`Order ${orderId} shipped`);
    this.notify(`Your order ${orderId} is on the way`);
  }
}

// Usage
const orderSystem = new OrderSystem();
const emailNotif = new EmailNotifier("user@example.com");
const smsNotif = new SMSNotifier("+1234567890");

orderSystem.attach(emailNotif);
orderSystem.attach(smsNotif);

orderSystem.placeOrder("ORD-001");
// Output:
// Order ORD-001 placed
// Email to user@example.com: Your order ORD-001 has been confirmed
// SMS to +1234567890: Your order ORD-001 has been confirmed

Pattern: Observer Pattern - subjects notify observers of state changes without tight coupling.


⚠️ Common Mistakes

1. Forgetting to Call super() in Subclass Constructor

// ❌ WRONG
class Dog extends Animal {
  constructor(name: string, breed: string) {
    // Error: Must call super() before accessing 'this'
    this.name = name;
  }
}

// βœ… RIGHT
class Dog extends Animal {
  constructor(name: string, private breed: string) {
    super(name); // Call parent constructor first!
  }
}

2. Misusing Access Modifiers

// ❌ WRONG
class User {
  private password: string;

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

class AdminUser extends User {
  showPassword(): void {
    console.log(this.password); // Error: 'password' is private
  }
}

// βœ… RIGHT
class User {
  protected password: string; // Use protected for subclass access
}

3. Not Implementing All Interface Members

// ❌ WRONG
interface Drawable {
  draw(): void;
  clear(): void;
}

class Circle implements Drawable {
  draw(): void {
    console.log("Drawing circle");
  }
  // Error: Class 'Circle' incorrectly implements interface 'Drawable'
  // Property 'clear' is missing
}

// βœ… RIGHT
class Circle implements Drawable {
  draw(): void {
    console.log("Drawing circle");
  }
  clear(): void {
    console.log("Clearing circle");
  }
}

4. Trying to Instantiate Abstract Classes

// ❌ WRONG
abstract class Vehicle {
  abstract move(): void;
}

const vehicle = new Vehicle(); // Error: Cannot create instance

// βœ… RIGHT
class Car extends Vehicle {
  move(): void {
    console.log("Driving");
  }
}

const car = new Car();

5. Confusing this Context in Methods

// ❌ WRONG
class Counter {
  count = 0;

  increment() {
    this.count++;
  }
}

const counter = new Counter();
const inc = counter.increment;
inc(); // Error: 'this' is undefined or refers to wrong object

// βœ… RIGHT (use arrow function or bind)
class Counter {
  count = 0;

  increment = () => {
    this.count++; // Arrow function preserves 'this'
  }
}

6. Overusing Inheritance Instead of Composition

// ❌ WRONG (Deep inheritance hierarchy)
class Vehicle {}
class LandVehicle extends Vehicle {}
class Car extends LandVehicle {}
class SportsCar extends Car {}
class RacingCar extends SportsCar {} // Too deep!

// βœ… RIGHT (Prefer composition)
interface Engine {
  start(): void;
}

class StandardEngine implements Engine {
  start(): void { console.log("Standard engine starting"); }
}

class TurboEngine implements Engine {
  start(): void { console.log("Turbo engine starting"); }
}

class Car {
  constructor(private engine: Engine) {}
  
  start(): void {
    this.engine.start();
  }
}

πŸ’‘ Remember: "Favor composition over inheritance" - it's more flexible and easier to maintain.


🎯 Key Takeaways

βœ… Classes encapsulate data and behavior into reusable blueprints

βœ… Access modifiers (public, private, protected) control visibility and encapsulation

βœ… Interfaces define contracts without implementation; classes provide the implementation

βœ… Inheritance (extends) promotes code reuse by creating parent-child relationships

βœ… Abstract classes provide partial implementation and force subclasses to complete it

βœ… Polymorphism allows different classes to be used interchangeably through interfaces or base classes

βœ… Getters/setters control property access and add validation logic

βœ… Static members belong to the class itself, not instances

βœ… Constructor shorthand (parameter properties) reduces boilerplate code

βœ… Composition over inheritance often leads to more maintainable code

πŸ€” Did you know? TypeScript's structural type system means that if two classes have the same structure, they're compatible even without explicit inheritance!


πŸ“š Further Study

  1. TypeScript Handbook - Classes: https://www.typescriptlang.org/docs/handbook/2/classes.html
  2. MDN - Object-Oriented Programming: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_programming
  3. Refactoring Guru - Design Patterns: https://refactoring.guru/design-patterns/typescript

πŸ“‹ Quick Reference Card

ConceptSyntaxPurpose
Class Declarationclass MyClass { }Define a blueprint for objects
Constructorconstructor(param: type) { }Initialize new instances
Public Propertypublic name: string;Accessible everywhere
Private Propertyprivate id: number;Only within class
Protected Propertyprotected data: any;Class and subclasses
Interfaceinterface IName { }Define contract/structure
Implementsclass X implements Y { }Fulfill interface contract
Extendsclass Child extends Parent { }Inherit from parent class
Abstract Classabstract class Base { }Cannot instantiate; partial implementation
Abstract Methodabstract method(): type;Must be implemented by subclass
Static Memberstatic prop: type;Belongs to class, not instance
Getterget prop(): type { }Computed property access
Setterset prop(value: type) { }Controlled property modification
Readonlyreadonly prop: type;Cannot be changed after initialization
Super Callsuper()Call parent constructor/method

🧠 Final Mnemonic: "C.I.E.A.P.S" - Classes define structure, Interfaces define contracts, Extends for inheritance, Abstract for partial implementation, Private for encapsulation, Static for shared utilities.

Practice Questions

Test your understanding with these questions:

Q1: Design a TypeScript class hierarchy for a library system with books and members. Include at least one interface, inheritance, and proper access modifiers. The system should track which member has borrowed which book.
A: !AI