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
superto 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
Vehiclewith properties:brand,model,year - Method
getDescription()returning a string Carclass extending Vehicle withnumDoorsMotorcycleclass extending Vehicle withengineCC
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
Notifiablewith methodsend(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
- Access modifiers control visibility:
public,private,protected - Inheritance with extends: Child classes inherit from parent classes
- super calls parent: Use in constructor and methods
- Override replaces behavior: Child methods can replace parent methods
- Abstract classes are templates: Cannot be instantiated, define contracts
- 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.