From Zero to AI

Lesson 4.5: Inheritance and Access Modifiers

Duration: 55 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Use access modifiers to control property and method visibility
  • Create class hierarchies using inheritance
  • Override methods in child classes
  • Use super to call parent class methods
  • Understand abstract classes and when to use them
  • Implement interfaces with classes

Access Modifiers

Access modifiers control who can access class members:

Modifier Class Subclass Outside
public Yes Yes Yes
protected Yes Yes No
private Yes No No

public (Default)

Public members are accessible everywhere:

class User {
  public name: string; // 'public' is the default
  email: string; // Also public (default)

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

  public greet(): void {
    console.log(`Hello, I'm ${this.name}`);
  }
}

const user = new User('Alice', 'alice@example.com');
console.log(user.name); // OK - public
console.log(user.email); // OK - public
user.greet(); // OK - public

private

Private members are only accessible within the class:

class BankAccount {
  private balance: number;
  private pin: string;

  constructor(initialBalance: number, pin: string) {
    this.balance = initialBalance;
    this.pin = pin;
  }

  // Public method to access private data
  getBalance(enteredPin: string): number | null {
    if (enteredPin === this.pin) {
      return this.balance;
    }
    return null;
  }

  // Private helper method
  private logTransaction(type: string, amount: number): void {
    console.log(`${type}: $${amount}`);
  }

  deposit(amount: number): void {
    this.balance += amount;
    this.logTransaction('Deposit', amount);
  }
}

const account = new BankAccount(1000, '1234');

// Cannot access private members
// console.log(account.balance); // Error! Property 'balance' is private
// console.log(account.pin);     // Error! Property 'pin' is private
// account.logTransaction();     // Error! Method is private

// Use public methods instead
console.log(account.getBalance('1234')); // 1000
account.deposit(500); // "Deposit: $500"

protected

Protected members are accessible in the class and its subclasses:

class Animal {
  protected name: string;
  private secret: string = 'hidden';

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

  protected makeSound(sound: string): void {
    console.log(`${this.name} says: ${sound}`);
  }
}

class Dog extends Animal {
  bark(): void {
    // Can access protected members from parent
    this.makeSound('Woof!');
    console.log(`${this.name} is barking`);

    // Cannot access private members from parent
    // console.log(this.secret); // Error! Property 'secret' is private
  }
}

const dog = new Dog('Buddy');
dog.bark(); // "Buddy says: Woof!" then "Buddy is barking"

// Cannot access protected from outside
// console.log(dog.name);     // Error! Property 'name' is protected
// dog.makeSound("Hello");    // Error! Method is protected

Inheritance Basics

Inheritance lets you create new classes based on existing ones:

// Parent class (base class / superclass)
class Vehicle {
  constructor(
    public brand: string,
    public year: number
  ) {}

  start(): void {
    console.log(`${this.brand} is starting...`);
  }

  stop(): void {
    console.log(`${this.brand} is stopping...`);
  }

  getInfo(): string {
    return `${this.year} ${this.brand}`;
  }
}

// Child class (derived class / subclass)
class Car extends Vehicle {
  constructor(
    brand: string,
    year: number,
    public numDoors: number
  ) {
    super(brand, year); // Call parent constructor
  }

  honk(): void {
    console.log(`${this.brand} goes beep beep!`);
  }
}

const car = new Car('Toyota', 2023, 4);
car.start(); // "Toyota is starting..." (inherited)
car.honk(); // "Toyota goes beep beep!" (own method)
console.log(car.getInfo()); // "2023 Toyota" (inherited)
console.log(car.numDoors); // 4 (own property)

The super Keyword

super refers to the parent class:

class Animal {
  constructor(public name: string) {}

  speak(): void {
    console.log(`${this.name} makes a sound`);
  }
}

class Cat extends Animal {
  constructor(
    name: string,
    public color: string
  ) {
    super(name); // Must call super() before using 'this'
  }

  speak(): void {
    super.speak(); // Call parent method
    console.log(`${this.name} meows`);
  }
}

const cat = new Cat('Whiskers', 'orange');
cat.speak();
// "Whiskers makes a sound"
// "Whiskers meows"

Method Overriding

Child classes can override parent methods:

class Shape {
  constructor(public name: string) {}

  getArea(): number {
    return 0;
  }

  describe(): string {
    return `This is a ${this.name}`;
  }
}

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

  // Override parent method
  getArea(): number {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor(public radius: number) {
    super('circle');
  }

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

  // Override and extend
  describe(): string {
    return `${super.describe()} with radius ${this.radius}`;
  }
}

const rect = new Rectangle(10, 20);
console.log(rect.getArea()); // 200
console.log(rect.describe()); // "This is a rectangle"

const circle = new Circle(5);
console.log(circle.getArea()); // 78.54...
console.log(circle.describe()); // "This is a circle with radius 5"

The override Keyword

Use override to explicitly mark overridden methods (catches typos):

class Parent {
  greet(): void {
    console.log('Hello from parent');
  }
}

class Child extends Parent {
  override greet(): void {
    console.log('Hello from child');
  }

  // Error if parent method doesn't exist
  // override greetings(): void {} // Error! Method doesn't exist in parent
}

Enable noImplicitOverride in tsconfig.json to require override:

{
  "compilerOptions": {
    "noImplicitOverride": true
  }
}

Abstract Classes

Abstract classes cannot be instantiated directly. They serve as base classes:

abstract class Animal {
  constructor(public name: string) {}

  // Regular method - can be inherited as-is
  eat(): void {
    console.log(`${this.name} is eating`);
  }

  // Abstract method - must be implemented by subclasses
  abstract makeSound(): void;

  // Abstract property
  abstract readonly species: string;
}

// Cannot create instance of abstract class
// const animal = new Animal("Generic"); // Error!

class Dog extends Animal {
  readonly species = 'Canis familiaris';

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

class Cat extends Animal {
  readonly species = 'Felis catus';

  makeSound(): void {
    console.log(`${this.name} meows: Meow!`);
  }
}

const dog = new Dog('Buddy');
dog.makeSound(); // "Buddy barks: Woof!"
dog.eat(); // "Buddy is eating"
console.log(dog.species); // "Canis familiaris"

const cat = new Cat('Whiskers');
cat.makeSound(); // "Whiskers meows: Meow!"

When to Use Abstract Classes

Use abstract classes when:

  • You want to share code among related classes
  • You need a common interface with some implementation
  • You want to enforce that certain methods exist
abstract class Database {
  protected connectionString: string;

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

  // Common implementation
  connect(): void {
    console.log(`Connecting to ${this.connectionString}`);
  }

  disconnect(): void {
    console.log('Disconnecting...');
  }

  // Each database implements these differently
  abstract query(sql: string): unknown[];
  abstract insert(table: string, data: unknown): void;
}

class MySQLDatabase extends Database {
  query(sql: string): unknown[] {
    console.log(`MySQL executing: ${sql}`);
    return [];
  }

  insert(table: string, data: unknown): void {
    console.log(`MySQL inserting into ${table}`);
  }
}

class PostgresDatabase extends Database {
  query(sql: string): unknown[] {
    console.log(`PostgreSQL executing: ${sql}`);
    return [];
  }

  insert(table: string, data: unknown): void {
    console.log(`PostgreSQL inserting into ${table}`);
  }
}

Implementing Interfaces

Classes can implement interfaces to guarantee they have certain properties and methods:

interface Printable {
  print(): void;
}

interface Saveable {
  save(): void;
  load(): void;
}

class Document implements Printable, Saveable {
  constructor(
    public title: string,
    public content: string
  ) {}

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

  save(): void {
    console.log(`Saving: ${this.title}`);
  }

  load(): void {
    console.log(`Loading: ${this.title}`);
  }
}

const doc = new Document('Report', 'This is the report content...');
doc.print();
doc.save();

Interface vs Abstract Class

Feature Interface Abstract Class
Implementation None Can have
Multiple Yes No (single inheritance)
Properties Type only Can have values
Constructor No Yes
// Interface - contract only
interface Flyable {
  fly(): void;
  altitude: number;
}

// Abstract class - partial implementation
abstract class Bird {
  constructor(public name: string) {}

  eat(): void {
    console.log(`${this.name} is eating`);
  }

  abstract chirp(): void;
}

// Combine both
class Eagle extends Bird implements Flyable {
  altitude: number = 0;

  chirp(): void {
    console.log('Screech!');
  }

  fly(): void {
    this.altitude = 1000;
    console.log(`${this.name} soars at ${this.altitude}m`);
  }
}

const eagle = new Eagle('Eddie');
eagle.eat(); // "Eddie is eating" (from Bird)
eagle.chirp(); // "Screech!" (implementing Bird)
eagle.fly(); // "Eddie soars at 1000m" (implementing Flyable)

Practical Examples

Example 1: Employee Hierarchy

abstract class Employee {
  constructor(
    public readonly id: number,
    public name: string,
    protected baseSalary: number
  ) {}

  abstract calculateSalary(): number;

  getInfo(): string {
    return `${this.name} (ID: ${this.id})`;
  }
}

class FullTimeEmployee extends Employee {
  constructor(
    id: number,
    name: string,
    baseSalary: number,
    private bonus: number = 0
  ) {
    super(id, name, baseSalary);
  }

  calculateSalary(): number {
    return this.baseSalary + this.bonus;
  }

  setBonus(amount: number): void {
    this.bonus = amount;
  }
}

class HourlyEmployee extends Employee {
  constructor(
    id: number,
    name: string,
    private hourlyRate: number,
    private hoursWorked: number = 0
  ) {
    super(id, name, 0);
  }

  calculateSalary(): number {
    return this.hourlyRate * this.hoursWorked;
  }

  logHours(hours: number): void {
    this.hoursWorked += hours;
  }
}

const fullTime = new FullTimeEmployee(1, 'Alice', 5000, 500);
console.log(fullTime.getInfo()); // "Alice (ID: 1)"
console.log(fullTime.calculateSalary()); // 5500

const hourly = new HourlyEmployee(2, 'Bob', 25);
hourly.logHours(160);
console.log(hourly.calculateSalary()); // 4000

Example 2: UI Components

interface Renderable {
  render(): string;
}

interface Clickable {
  onClick(handler: () => void): void;
}

abstract class UIComponent implements Renderable {
  protected visible: boolean = true;

  constructor(public id: string) {}

  show(): void {
    this.visible = true;
  }

  hide(): void {
    this.visible = false;
  }

  abstract render(): string;
}

class Button extends UIComponent implements Clickable {
  private clickHandler?: () => void;

  constructor(
    id: string,
    public label: string
  ) {
    super(id);
  }

  onClick(handler: () => void): void {
    this.clickHandler = handler;
  }

  click(): void {
    if (this.clickHandler) {
      this.clickHandler();
    }
  }

  render(): string {
    if (!this.visible) return '';
    return `<button id="${this.id}">${this.label}</button>`;
  }
}

class TextInput extends UIComponent {
  private value: string = '';

  constructor(
    id: string,
    public placeholder: string
  ) {
    super(id);
  }

  setValue(value: string): void {
    this.value = value;
  }

  getValue(): string {
    return this.value;
  }

  render(): string {
    if (!this.visible) return '';
    return `<input id="${this.id}" placeholder="${this.placeholder}" value="${this.value}" />`;
  }
}

const button = new Button('submit-btn', 'Submit');
button.onClick(() => console.log('Clicked!'));
console.log(button.render()); // <button id="submit-btn">Submit</button>

const input = new TextInput('email-input', 'Enter email');
input.setValue('alice@example.com');
console.log(input.render()); // <input id="email-input" placeholder="Enter email" value="alice@example.com" />

Example 3: Payment Processors

interface PaymentResult {
  success: boolean;
  transactionId?: string;
  error?: string;
}

abstract class PaymentProcessor {
  constructor(protected apiKey: string) {}

  abstract processPayment(amount: number): PaymentResult;

  protected generateTransactionId(): string {
    return `TXN-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  protected log(message: string): void {
    console.log(`[${this.constructor.name}] ${message}`);
  }
}

class StripeProcessor extends PaymentProcessor {
  processPayment(amount: number): PaymentResult {
    this.log(`Processing $${amount} via Stripe`);

    // Simulated processing
    if (amount > 0) {
      return {
        success: true,
        transactionId: this.generateTransactionId(),
      };
    }

    return {
      success: false,
      error: 'Invalid amount',
    };
  }
}

class PayPalProcessor extends PaymentProcessor {
  processPayment(amount: number): PaymentResult {
    this.log(`Processing $${amount} via PayPal`);

    if (amount > 0) {
      return {
        success: true,
        transactionId: this.generateTransactionId(),
      };
    }

    return {
      success: false,
      error: 'Invalid amount',
    };
  }
}

const stripe = new StripeProcessor('sk_test_xxx');
const result = stripe.processPayment(99.99);
console.log(result);
// { success: true, transactionId: "TXN-1234567890-abc123def" }

Exercises

Exercise 1: Vehicle Hierarchy

Create a vehicle hierarchy with:

  • Base class Vehicle with properties: brand, model, year
  • Method getDescription() returning a string
  • Car class extending Vehicle with numDoors
  • Motorcycle class extending Vehicle with engineCC
Solution
class Vehicle {
  constructor(
    public brand: string,
    public model: string,
    public year: number
  ) {}

  getDescription(): string {
    return `${this.year} ${this.brand} ${this.model}`;
  }
}

class Car extends Vehicle {
  constructor(
    brand: string,
    model: string,
    year: number,
    public numDoors: number
  ) {
    super(brand, model, year);
  }

  override getDescription(): string {
    return `${super.getDescription()} (${this.numDoors}-door)`;
  }
}

class Motorcycle extends Vehicle {
  constructor(
    brand: string,
    model: string,
    year: number,
    public engineCC: number
  ) {
    super(brand, model, year);
  }

  override getDescription(): string {
    return `${super.getDescription()} (${this.engineCC}cc)`;
  }
}

const car = new Car('Toyota', 'Camry', 2023, 4);
console.log(car.getDescription()); // "2023 Toyota Camry (4-door)"

const bike = new Motorcycle('Honda', 'CBR', 2023, 600);
console.log(bike.getDescription()); // "2023 Honda CBR (600cc)"

Exercise 2: Shape Calculator

Create an abstract Shape class with:

  • Abstract methods: getArea(), getPerimeter()
  • Concrete subclasses: Rectangle, Circle, Triangle
Solution
abstract class Shape {
  abstract getArea(): number;
  abstract getPerimeter(): number;

  describe(): string {
    return `Area: ${this.getArea().toFixed(2)}, Perimeter: ${this.getPerimeter().toFixed(2)}`;
  }
}

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

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

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

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

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

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

class Triangle extends Shape {
  constructor(
    private a: number,
    private b: number,
    private c: number
  ) {
    super();
  }

  getArea(): number {
    // Heron's formula
    const s = (this.a + this.b + this.c) / 2;
    return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
  }

  getPerimeter(): number {
    return this.a + this.b + this.c;
  }
}

const rect = new Rectangle(10, 5);
console.log(rect.describe()); // "Area: 50.00, Perimeter: 30.00"

const circle = new Circle(7);
console.log(circle.describe()); // "Area: 153.94, Perimeter: 43.98"

const triangle = new Triangle(3, 4, 5);
console.log(triangle.describe()); // "Area: 6.00, Perimeter: 12.00"

Exercise 3: Notification System

Create a notification system with:

  • Interface Notifiable with method send(message: string): boolean
  • Classes: EmailNotification, SMSNotification, PushNotification
  • Each with appropriate constructor parameters
Solution
interface Notifiable {
  send(message: string): boolean;
}

class EmailNotification implements Notifiable {
  constructor(
    private to: string,
    private subject: string
  ) {}

  send(message: string): boolean {
    console.log(`Sending email to ${this.to}`);
    console.log(`Subject: ${this.subject}`);
    console.log(`Body: ${message}`);
    return true;
  }
}

class SMSNotification implements Notifiable {
  constructor(private phoneNumber: string) {}

  send(message: string): boolean {
    if (message.length > 160) {
      console.log('Message too long for SMS');
      return false;
    }
    console.log(`Sending SMS to ${this.phoneNumber}: ${message}`);
    return true;
  }
}

class PushNotification implements Notifiable {
  constructor(
    private deviceToken: string,
    private title: string
  ) {}

  send(message: string): boolean {
    console.log(`Sending push to device ${this.deviceToken}`);
    console.log(`Title: ${this.title}`);
    console.log(`Body: ${message}`);
    return true;
  }
}

// Usage
const email = new EmailNotification('user@example.com', 'Welcome!');
email.send('Thank you for signing up!');

const sms = new SMSNotification('+1234567890');
sms.send('Your code is 123456');

const push = new PushNotification('device-token-xyz', 'New Message');
push.send('You have a new message');

Key Takeaways

  1. Access modifiers control visibility: public, private, protected
  2. Inheritance with extends: Child classes inherit from parent classes
  3. super calls parent: Use in constructor and methods
  4. Override replaces behavior: Child methods can replace parent methods
  5. Abstract classes are templates: Cannot be instantiated, define contracts
  6. Interfaces define contracts: Classes can implement multiple interfaces

Resources

Resource Type Description
TypeScript Handbook: Classes Documentation Complete class guide
TypeScript: Member Visibility Documentation Access modifiers explained
MDN: Inheritance Documentation JavaScript inheritance concepts

Next Lesson

Now that you understand classes and inheritance, let us put it all together by building a complete TodoList application.

Continue to Lesson 4.6: Practice - TodoList Class