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
Personclass with propertiesnameandage. - 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
Bookclass withtitle,author, andyear. - 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
Rectangleclass withwidthandheight. - 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
Studentclass withnameandgrades(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
BankAccountclass with a privatebalancefield. - Add methods:
deposit(),withdraw(), andgetBalance(). - 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
Carclass with privatemileage. - Add methods
drive(distance)andgetMileage(). - 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
Circleclass with a privatepiconstant. - Add methods
getArea()andgetCircumference(). - 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
privateto hide implementation details (encapsulation) - Use
readonlyfor 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
privateby default, make public only what's needed - Use parameter properties for simple property initialization
- Prefer
readonlyfor 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 →