Skip to main content

Classes & Constructors

Introduction​

Classes are blueprints for creating objects . They let you define the structure and behavior that multiple objects will share. Think of a class as a cookie cutter and objects as the cookies - one template creates many similar instances.

In object-oriented programming, classes help you organize code by grouping related data (properties) and functionality (methods) together. This makes your code more maintainable, reusable, and easier to understand.

TypeScript takes classes to the next level by adding:

  • Access modifiers (public, private, protected)
  • Type safety for properties and methods
  • Interfaces that classes can implement
  • Abstract classes for defining contracts
  • Parameter properties for cleaner constructors

Why Classes Matter​

Without classes (repetitive):

let person1 = {
name: "Alice",
age: 25,
greet: function() { return `Hi, I'm ${this.name}`; }
};

let person2 = {
name: "Bob",
age: 30,
greet: function() { return `Hi, I'm ${this.name}`; }
};
// Duplicating the greet method for every person!

With classes (organized):

class Person {
name: string;
age: number;

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

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

let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);
// One blueprint, many instances!

TypeScript's Type Safety for Classes​

JavaScript (no type checking):

class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}

let person = new Person("Alice", "twenty-five"); // Oops! Age is a string

TypeScript (with type checking):

class Person {
name: string;
age: number;

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

// let person = new Person("Alice", "twenty-five"); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'
let person = new Person("Alice", 25); // ✅ Correct

Creating Classes​

Basic Class Syntax​

class Person {
// Properties (instance variables)
name: string;
age: number;

// Constructor (runs when creating a new instance)
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

// Method (function that belongs to the class)
greet(): string {
return `Hello, I'm ${this.name} and I'm ${this.age} years old.`;
}
}

// Creating instances (objects from the class)
let alice = new Person("Alice", 25);
let bob = new Person("Bob", 30);

console.log(alice.greet()); // "Hello, I'm Alice and I'm 25 years old."
console.log(bob.greet()); // "Hello, I'm Bob and I'm 30 years old."

Understanding this​

Inside a class, this refers to the current instance:

class Counter {
count: number;

constructor(startValue: number = 0) {
this.count = startValue; // 'this' refers to the instance being created
}

increment(): void {
this.count++; // 'this' refers to the instance calling the method
}

getValue(): number {
return this.count;
}
}

let counter1 = new Counter(0);
let counter2 = new Counter(100);

counter1.increment();
counter2.increment();

console.log(counter1.getValue()); // 1
console.log(counter2.getValue()); // 101
// Each instance has its own 'count' property

Constructors​

The constructor is a special method that runs automatically when you create a new instance.

Basic Constructor​

class Book {
title: string;
author: string;
year: number;

constructor(title: string, author: string, year: number) {
this.title = title;
this.author = author;
this.year = year;
}
}

let book = new Book("1984", "George Orwell", 1949);

Constructor with Default Parameters​

class User {
username: string;
email: string;
role: string;

constructor(username: string, email: string, role: string = "user") {
this.username = username;
this.email = email;
this.role = role;
}
}

let user1 = new User("alice", "alice@email.com"); // role: "user"
let user2 = new User("admin", "admin@email.com", "admin"); // role: "admin"

Parameter Properties (TypeScript Shortcut)​

TypeScript lets you declare and initialize properties directly in the constructor:

// Long way:
class Person1 {
name: string;
age: number;

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

// Short way (parameter properties):
class Person2 {
constructor(public name: string, public age: number) {
// Properties automatically created and initialized!
}
}

let person = new Person2("Alice", 25);
console.log(person.name); // "Alice"
console.log(person.age); // 25

This is much cleaner and reduces boilerplate code!


Access Modifiers​

TypeScript provides three access modifiers to control property and method visibility:

Public (Default)​

Accessible from anywhere:

class Person {
public name: string; // 'public' is default, can be omitted

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

let person = new Person("Alice");
console.log(person.name); // ✅ Accessible
person.name = "Alicia"; // ✅ Can modify

Private​

Only accessible within the class:

class BankAccount {
private balance: number;

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

deposit(amount: number): void {
this.balance += amount; // ✅ Can access inside the class
}

getBalance(): number {
return this.balance; // ✅ Can access inside the class
}
}

let account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // ✅ 1500

// console.log(account.balance); // ❌ Error: Property 'balance' is private
// account.balance = 9999; // ❌ Error: Property 'balance' is private

Protected​

Accessible within the class and subclasses (inheritance):

class Person {
protected ssn: string;

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

class Employee extends Person {
constructor(ssn: string, public employeeId: string) {
super(ssn);
}

displaySSN(): string {
return this.ssn; // ✅ Can access in subclass
}
}

let emp = new Employee("123-45-6789", "E001");
console.log(emp.displaySSN()); // ✅ Works
// console.log(emp.ssn); // ❌ Error: Property 'ssn' is protected

Private Fields with​

JavaScript also supports private fields with # (works at runtime):

class Secret {
#password: string; // Private field (runtime privacy)

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

checkPassword(attempt: string): boolean {
return this.#password === attempt;
}
}

let secret = new Secret("super-secret");
console.log(secret.checkPassword("super-secret")); // true
// console.log(secret.#password); // ❌ Syntax error: Private field

Difference between private and #:

  • private: TypeScript compile-time check only (removed in JavaScript output)
  • #: JavaScript runtime privacy (truly private even in compiled code)

Readonly Properties​

Properties that can only be set in the constructor:

class Person {
readonly id: number;
name: string;

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

updateName(newName: string): void {
this.name = newName; // ✅ Can modify name
// this.id = 999; // ❌ Error: Cannot assign to 'id' because it is a read-only property
}
}

let person = new Person(1, "Alice");
person.name = "Alicia"; // ✅ Can change name
// person.id = 2; // ❌ Error: Cannot assign to 'id'

Methods​

Functions inside classes are called methods:

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

subtract(a: number, b: number): number {
return a - b;
}

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

divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
}

let calc = new Calculator();
console.log(calc.add(5, 3)); // 8
console.log(calc.multiply(4, 7)); // 28

Methods with this​

class ShoppingCart {
private items: string[] = [];

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

removeItem(item: string): void {
const index = this.items.indexOf(item);
if (index > -1) {
this.items.splice(index, 1);
}
}

getItems(): string[] {
return [...this.items]; // Return a copy
}

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

let cart = new ShoppingCart();
cart.addItem("Apple");
cart.addItem("Banana");
console.log(cart.getItemCount()); // 2

Getters and Setters​

Control access to properties with validation:

class Employee {
private _salary: number;

constructor(public name: string, salary: number) {
this._salary = salary;
}

// Getter (read property like: emp.salary)
get salary(): number {
return this._salary;
}

// Setter (write property like: emp.salary = 50000)
set salary(amount: number) {
if (amount < 0) {
throw new Error("Salary cannot be negative");
}
this._salary = amount;
}
}

let emp = new Employee("Alice", 50000);
console.log(emp.salary); // 50000 (uses getter)

emp.salary = 60000; // ✅ Uses setter
console.log(emp.salary); // 60000

// emp.salary = -1000; // ❌ Error: Salary cannot be negative

Computed Properties with Getters​

class Rectangle {
constructor(public width: number, public height: number) {}

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

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

let rect = new Rectangle(10, 5);
console.log(rect.area); // 50 (computed on demand)
console.log(rect.perimeter); // 30 (computed on demand)

Static Members​

Properties and methods that belong to the class itself, not instances:

class MathHelper {
static PI: number = 3.14159;

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

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

// Access without creating an instance
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.circleArea(5)); // 78.53975
console.log(MathHelper.circleCircumference(5)); // 31.4159

// Can't access static members on instances
// let helper = new MathHelper();
// console.log(helper.PI); // ❌ Error

Tracking Instances with Static​

class BankAccount {
private static accountCount: number = 0;
private balance: number;

constructor(public owner: string, initialBalance: number) {
this.balance = initialBalance;
BankAccount.accountCount++;
}

static getTotalAccounts(): number {
return BankAccount.accountCount;
}

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

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

let acc1 = new BankAccount("Alice", 1000);
let acc2 = new BankAccount("Bob", 500);
let acc3 = new BankAccount("Charlie", 2000);

console.log(BankAccount.getTotalAccounts()); // 3

Inheritance​

Classes can extend other classes:

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

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

class Dog extends Animal {
bark(): void {
console.log("Woof! Woof!");
}
}

class Cat extends Animal {
meow(): void {
console.log("Meow!");
}
}

let dog = new Dog("Rex");
dog.move(10); // "Rex moved 10 meters."
dog.bark(); // "Woof! Woof!"

let cat = new Cat("Whiskers");
cat.move(5); // "Whiskers moved 5 meters."
cat.meow(); // "Meow!"

Calling Parent Constructor with super()​

class Person {
constructor(public name: string, public age: number) {}
}

class Employee extends Person {
constructor(name: string, age: number, public employeeId: string) {
super(name, age); // Call parent constructor
}

getDetails(): string {
return `${this.name} (${this.age}), ID: ${this.employeeId}`;
}
}

let emp = new Employee("Alice", 30, "E001");
console.log(emp.getDetails()); // "Alice (30), ID: E001"

Method Overriding​

class Animal {
makeSound(): void {
console.log("Some generic sound");
}
}

class Dog extends Animal {
makeSound(): void { // Override parent method
console.log("Woof!");
}
}

class Cat extends Animal {
makeSound(): void { // Override parent method
console.log("Meow!");
}
}

let animals: Animal[] = [new Dog(), new Cat()];
animals.forEach(animal => animal.makeSound());
// "Woof!"
// "Meow!"

Abstract Classes​

Define a contract that subclasses must implement:

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

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

// Regular method (inherited by subclasses)
describe(): string {
return `This is a ${this.name} with area ${this.getArea()}`;
}
}

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

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

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

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

// let shape = new Shape("Generic"); // ❌ Error: Cannot create an instance of an abstract class

let circle = new Circle(5);
let rectangle = new Rectangle(10, 5);

console.log(circle.describe()); // "This is a Circle with area 78.53981633974483"
console.log(rectangle.describe()); // "This is a Rectangle with area 50"

Implementing Interfaces​

Classes can implement interfaces to ensure they have specific properties and methods:

interface Flyable {
fly(): void;
altitude: number;
}

interface Swimmable {
swim(): void;
}

class Bird implements Flyable {
altitude: number = 0;

fly(): void {
this.altitude += 100;
console.log(`Flying at ${this.altitude} meters`);
}
}

class Duck implements Flyable, Swimmable {
altitude: number = 0;

fly(): void {
this.altitude += 50;
console.log(`Duck flying at ${this.altitude} meters`);
}

swim(): void {
console.log("Duck swimming");
}
}

let bird = new Bird();
bird.fly(); // "Flying at 100 meters"

let duck = new Duck();
duck.fly(); // "Duck flying at 50 meters"
duck.swim(); // "Duck swimming"

Concepts​

Quick Reference​

// Basic class
class Person {
name: string;
age: number;

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

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

// Parameter properties
class User {
constructor(public username: string, private password: string) {}
}

// Access modifiers
class Example {
public publicProp: string;
private privateProp: string;
protected protectedProp: string;
readonly readonlyProp: string;
}

// Static members
class Helper {
static count: number = 0;
static increment(): void {
Helper.count++;
}
}

// Inheritance
class Child extends Parent {
constructor() {
super(); // Call parent constructor
}
}

// Abstract class
abstract class Base {
abstract method(): void;
}

// Implementing interface
class MyClass implements MyInterface {
// Must implement all interface members
}

Examples​

// User management system
class User {
private static userCount: number = 0;
readonly id: number;

constructor(
public username: string,
private email: string,
public role: "admin" | "user" = "user"
) {
this.id = ++User.userCount;
}

getEmail(): string {
return this.email;
}

static getTotalUsers(): number {
return User.userCount;
}
}

// Product catalog
class Product {
constructor(
public id: string,
public name: string,
private price: number,
public inStock: boolean = true
) {}

get displayPrice(): string {
return `$${this.price.toFixed(2)}`;
}

set displayPrice(value: string) {
const num = parseFloat(value.replace("$", ""));
if (num >= 0) {
this.price = num;
}
}

applyDiscount(percent: number): void {
this.price *= (1 - percent / 100);
}
}

Challenges​

1. Create Person Class​

  • Create a Person class with properties name and age.
  • Add proper type annotations.
  • Create two instances and print their details.
💡 Show Answer
class Person {
name: string;
age: number;

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

let person1 = new Person("James", 38);
let person2 = new Person("Susan", 22);

console.log(person1); // Person { name: 'James', age: 38 }
console.log(person2); // Person { name: 'Susan', age: 22 }

// Using parameter properties (shorter)
class PersonShort {
constructor(public name: string, public age: number) {}
}

let person3 = new PersonShort("Alice", 25);
console.log(person3); // PersonShort { name: 'Alice', age: 25 }

// With toString method
class PersonWithString {
constructor(public name: string, public age: number) {}

toString(): string {
return `${this.name} (${this.age} years old)`;
}
}

let person4 = new PersonWithString("Bob", 30);
console.log(person4.toString()); // "Bob (30 years old)"


2. Add introduce Method​

  • Add a method introduce() to Person.
  • It should return a string like "My name is [name] and I am [age] years old."
  • Call the method on different instances.
💡 Show Answer
class Person {
constructor(public name: string, public age: number) {}

introduce(): string {
return `My name is ${this.name} and I am ${this.age} years old.`;
}
}

let alice = new Person("Alice", 25);
let bob = new Person("Bob", 30);

console.log(alice.introduce());
// "My name is Alice and I am 25 years old."

console.log(bob.introduce());
// "My name is Bob and I am 30 years old."

// Extended version with additional info
class PersonExtended extends Person {
constructor(name: string, age: number, public occupation: string) {
super(name, age);
}

introduce(): string {
return `${super.introduce()} I work as a ${this.occupation}.`;
}
}

let charlie = new PersonExtended("Charlie", 35, "Developer");
console.log(charlie.introduce());
// "My name is Charlie and I am 35 years old. I work as a Developer."

3. Book Class with getSummary​

  • Create a Book class with title, author, and year.
  • Add a getSummary() method that returns a formatted string.
  • Create a few book instances.
💡 Show Answer
class Book {
constructor(
public title: string,
public author: string,
public year: number
) {}

getSummary(): string {
return `"${this.title}" by ${this.author} (${this.year})`;
}

getAge(): number {
return new Date().getFullYear() - this.year;
}
}

let book1 = new Book("1984", "George Orwell", 1949);
let book2 = new Book("To Kill a Mockingbird", "Harper Lee", 1960);

console.log(book1.getSummary());
// "1984" by George Orwell (1949)

console.log(book2.getSummary());
// "To Kill a Mockingbird" by Harper Lee (1960)

console.log(`${book1.title} is ${book1.getAge()} years old`);

// Library management
class Library {
private books: Book[] = [];

addBook(book: Book): void {
this.books.push(book);
}

listBooks(): void {
this.books.forEach((book, index) => {
console.log(`${index + 1}. ${book.getSummary()}`);
});
}

findByAuthor(author: string): Book[] {
return this.books.filter(book => book.author === author);
}
}

let library = new Library();
library.addBook(book1);
library.addBook(book2);
library.listBooks();


4. Rectangle with getArea​

  • Create a Rectangle class with width and height.
  • Add a getArea() method.
  • Add proper type annotations.
💡 Show Answer
class Rectangle {
constructor(public width: number, public height: number) {}

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

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

isSquare(): boolean {
return this.width === this.height;
}

scale(factor: number): void {
this.width *= factor;
this.height *= factor;
}
}

let rect1 = new Rectangle(10, 5);
console.log(rect1.getArea()); // 50
console.log(rect1.getPerimeter()); // 30
console.log(rect1.isSquare()); // false

let square = new Rectangle(5, 5);
console.log(square.isSquare()); // true

// Using getters for computed properties
class RectangleWithGetters {
constructor(public width: number, public height: number) {}

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

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

let rect2 = new RectangleWithGetters(10, 5);
console.log(rect2.area); // 50 (no parentheses!)
console.log(rect2.perimeter); // 30

5. Add getPerimeter Method​

  • Extend the Rectangle class with a getPerimeter() method.
  • Formula: 2 × (width + height)
  • Test with different rectangles.
💡 Show Answer
class Rectangle {
constructor(public width: number, public height: number) {}

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

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

getDiagonal(): number {
return Math.sqrt(this.width ** 2 + this.height ** 2);
}

toString(): string {
return `Rectangle(${this.width} × ${this.height})`;
}
}

let rect = new Rectangle(10, 5);
console.log(`Area: ${rect.getArea()}`); // 50
console.log(`Perimeter: ${rect.getPerimeter()}`); // 30
console.log(`Diagonal: ${rect.getDiagonal().toFixed(2)}`); // 11.18
console.log(rect.toString()); // "Rectangle(10 × 5)"

// Compare rectangles
class RectangleComparator {
static larger(r1: Rectangle, r2: Rectangle): Rectangle {
return r1.getArea() > r2.getArea() ? r1 : r2;
}

static combine(r1: Rectangle, r2: Rectangle): number {
return r1.getArea() + r2.getArea();
}
}

let rect1 = new Rectangle(10, 5);
let rect2 = new Rectangle(8, 6);

let larger = RectangleComparator.larger(rect1, rect2);
console.log(`Larger rectangle: ${larger.toString()}`);


6. Student with calculateAverage​

  • Create a Student class with name and grades (array of numbers).
  • Add a calculateAverage() method.
  • Add methods to add grades and check if passing.
💡 Show Answer
class Student {
constructor(public name: string, private grades: number[] = []) {}

addGrade(grade: number): void {
if (grade >= 0 && grade <= 100) {
this.grades.push(grade);
} else {
throw new Error("Grade must be between 0 and 100");
}
}

calculateAverage(): number {
if (this.grades.length === 0) return 0;
const sum = this.grades.reduce((a, b) => a + b, 0);
return sum / this.grades.length;
}

isPassing(passingGrade: number = 60): boolean {
return this.calculateAverage() >= passingGrade;
}

getLetterGrade(): string {
const avg = this.calculateAverage();
if (avg >= 90) return "A";
if (avg >= 80) return "B";
if (avg >= 70) return "C";
if (avg >= 60) return "D";
return "F";
}

getGrades(): number[] {
return [...this.grades]; // Return copy
}

getHighestGrade(): number {
return Math.max(...this.grades);
}

getLowestGrade(): number {
return Math.min(...this.grades);
}

getReport(): string {
return `
Student: ${this.name}
Average: ${this.calculateAverage().toFixed(2)}
Letter Grade: ${this.getLetterGrade()}
Passing: ${this.isPassing() ? "Yes" : "No"}
Grades: ${this.grades.join(", ")}
`.trim();
}
}

let student = new Student("Alice");
student.addGrade(85);
student.addGrade(90);
student.addGrade(78);
student.addGrade(92);

console.log(student.getReport());
console.log(`Highest: ${student.getHighestGrade()}`); // 92
console.log(`Lowest: ${student.getLowestGrade()}`); // 78

7. BankAccount with Private Balance​

  • Create a BankAccount class with a private balance field.
  • Add methods: deposit(), withdraw(), and getBalance().
  • Prevent negative withdrawals.
💡 Show Answer
class BankAccount {
private balance: number;

constructor(public owner: string, initialBalance: number = 0) {
if (initialBalance < 0) {
throw new Error("Initial balance cannot be negative");
}
this.balance = initialBalance;
}

deposit(amount: number): void {
if (amount <= 0) {
throw new Error("Deposit amount must be positive");
}
this.balance += amount;
console.log(`Deposited $${amount}. New balance: $${this.balance}`);
}

withdraw(amount: number): void {
if (amount <= 0) {
throw new Error("Withdrawal amount must be positive");
}
if (amount > this.balance) {
throw new Error("Insufficient funds");
}
this.balance -= amount;
console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
}

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

transfer(amount: number, toAccount: BankAccount): void {
this.withdraw(amount);
toAccount.deposit(amount);
console.log(`Transferred $${amount} to ${toAccount.owner}`);
}
}

let account1 = new BankAccount("Alice", 1000);
let account2 = new BankAccount("Bob", 500);

account1.deposit(500); // Deposited $500. New balance: $1500
account1.withdraw(200); // Withdrew $200. New balance: $1300
account1.transfer(300, account2); // Transfer

console.log(account1.getBalance()); // 1000
console.log(account2.getBalance()); // 800

// Can't access private balance
// console.log(account1.balance); // ❌ Error: Property 'balance' is private

// Enhanced with transaction history
class BankAccountWithHistory extends BankAccount {
private transactions: string[] = [];

deposit(amount: number): void {
super.deposit(amount);
this.transactions.push(`+$${amount}`);
}

withdraw(amount: number): void {
super.withdraw(amount);
this.transactions.push(`-$${amount}`);
}

getTransactionHistory(): string[] {
return [...this.transactions];
}
}


8. Car with Private Mileage​

  • Create a Car class with private mileage.
  • Add methods drive(distance) and getMileage().
  • Prevent negative distances.
💡 Show Answer
class Car {
private mileage: number = 0;

constructor(public make: string, public model: string, public year: number) {}

drive(distance: number): void {
if (distance < 0) {
throw new Error("Distance cannot be negative");
}
this.mileage += distance;
console.log(`Drove ${distance} km. Total mileage: ${this.mileage} km`);
}

getMileage(): number {
return this.mileage;
}

getInfo(): string {
return `${this.year} ${this.make} ${this.model} - ${this.mileage} km`;
}

needsService(): boolean {
return this.mileage >= 10000;
}

resetMileage(): void {
console.log(`Service completed. Resetting mileage from ${this.mileage} to 0`);
this.mileage = 0;
}
}

let car = new Car("Toyota", "Camry", 2020);

car.drive(150); // Drove 150 km
car.drive(200); // Drove 200 km
car.drive(50); // Drove 50 km

console.log(car.getInfo()); // "2020 Toyota Camry - 400 km"
console.log(`Needs service: ${car.needsService()}`); // false

// Can't access private mileage
// console.log(car.mileage); // ❌ Error: Property 'mileage' is private

// Fleet management
class Fleet {
private cars: Car[] = [];

addCar(car: Car): void {
this.cars.push(car);
}

getTotalMileage(): number {
return this.cars.reduce((sum, car) => sum + car.getMileage(), 0);
}

getCarsNeedingService(): Car[] {
return this.cars.filter(car => car.needsService());
}
}

9. Demonstrate Private Field Access Error​

  • Try to access a private field directly from outside the class.
  • Show the TypeScript error message.
  • Explain why encapsulation is important.
💡 Show Answer
class Secret {
private secretCode: string;

constructor(code: string) {
this.secretCode = code;
}

checkCode(attempt: string): boolean {
return this.secretCode === attempt;
}

// No getter for secretCode - truly private!
}

let secret = new Secret("12345");

// ✅ This works - using public method
console.log(secret.checkCode("12345")); // true
console.log(secret.checkCode("99999")); // false

// ❌ This fails - trying to access private property
// console.log(secret.secretCode);
// Error: Property 'secretCode' is private and only accessible within class 'Secret'

// Why encapsulation matters:
class BankAccount {
private balance: number;

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

// Controlled access with validation
withdraw(amount: number): void {
if (amount > this.balance) {
throw new Error("Insufficient funds");
}
this.balance -= amount;
}

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

let account = new BankAccount(100);

// ✅ Safe - goes through validation
try {
account.withdraw(150); // Throws error
} catch (e) {
console.log("Withdrawal blocked");
}

// ❌ If balance were public, someone could do:
// account.balance = -1000; // Bypasses all validation!
// This is why we use private fields

// Real-world example: Password storage
class UserAccount {
private passwordHash: string;

constructor(public username: string, password: string) {
this.passwordHash = this.hashPassword(password);
}

private hashPassword(password: string): string {
// In real app, use bcrypt or similar
return btoa(password); // Simple example only!
}

checkPassword(attempt: string): boolean {
return this.passwordHash === this.hashPassword(attempt);
}

// Notice: No way to get the password back!
// This is security through encapsulation
}

let user = new UserAccount("alice", "secret123");
console.log(user.checkPassword("secret123")); // true
// console.log(user.passwordHash); // ❌ Error: private


10. Circle with Private Pi​

  • Create a Circle class with a private pi constant.
  • Add methods getArea() and getCircumference().
  • Use proper type annotations throughout.
💡 Show Answer
class Circle {
private readonly pi: number = Math.PI;

constructor(private radius: number) {
if (radius <= 0) {
throw new Error("Radius must be positive");
}
}

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

getCircumference(): number {
return 2 * this.pi * this.radius;
}

getDiameter(): number {
return 2 * this.radius;
}

getRadius(): number {
return this.radius;
}

scale(factor: number): void {
if (factor <= 0) {
throw new Error("Scale factor must be positive");
}
this.radius *= factor;
}

toString(): string {
return `Circle(radius: ${this.radius})`;
}
}

let circle = new Circle(5);

console.log(`Area: ${circle.getArea().toFixed(2)}`); // 78.54
console.log(`Circumference: ${circle.getCircumference().toFixed(2)}`); // 31.42
console.log(`Diameter: ${circle.getDiameter()}`); // 10

circle.scale(2);
console.log(`New area: ${circle.getArea().toFixed(2)}`); // 314.16

// Can't access private fields
// console.log(circle.pi); // ❌ Error: private
// console.log(circle.radius); // ❌ Error: private

// Cylinder that extends Circle
class Cylinder extends Circle {
constructor(radius: number, private height: number) {
super(radius);
}

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

getSurfaceArea(): number {
return 2 * this.getArea() + this.getCircumference() * this.height;
}
}

let cylinder = new Cylinder(5, 10);
console.log(`Volume: ${cylinder.getVolume().toFixed(2)}`); // 785.40
console.log(`Surface area: ${cylinder.getSurfaceArea().toFixed(2)}`); // 471.24

// Shape comparison with static method
class CircleComparator {
static larger(c1: Circle, c2: Circle): Circle {
return c1.getArea() > c2.getArea() ? c1 : c2;
}

static totalArea(...circles: Circle[]): number {
return circles.reduce((sum, c) => sum + c.getArea(), 0);
}
}

let c1 = new Circle(5);
let c2 = new Circle(10);
let larger = CircleComparator.larger(c1, c2);
console.log(`Larger circle radius: ${larger.getRadius()}`); // 10

console.log(`Total area: ${CircleComparator.totalArea(c1, c2).toFixed(2)}`);

Summary​

In this chapter, you learned:

✅ How to create classes as blueprints for objects

✅ Using constructors to initialize instances

✅ Access modifiers: public, private, protected

✅ Parameter properties for cleaner code

✅ Readonly properties for immutable data

✅ Instance methods and the this keyword

✅ Getters and setters for controlled access

✅ Static members that belong to the class

✅ Inheritance with extends and super

✅ Abstract classes for defining contracts

✅ Implementing interfaces for type safety

Key Takeaways:

  • Classes group related data and behavior together
  • Use private to hide implementation details (encapsulation)
  • Use readonly for data that shouldn't change after initialization
  • Parameter properties reduce boilerplate code
  • Static members are shared across all instances
  • Inheritance creates "is-a" relationships
  • Interfaces define contracts that classes must fulfill

Best Practices:

  • Start with private by default, make public only what's needed
  • Use parameter properties for simple property initialization
  • Prefer readonly for IDs and other immutable data
  • Use getters/setters when you need validation or computed values
  • Keep classes focused on a single responsibility
  • Use inheritance sparingly - favor composition over inheritance
  • Implement interfaces to ensure your classes have required functionality

In the next chapter, we'll explore Static Functions & Variables in more depth!

Continue to Chapter 9: Static Functions & Variables →