Object-Oriented TypeScript
Implement OOP concepts including classes, inheritance, polymorphism, and access modifiers for structured code architecture.
Object-Oriented TypeScript
Master object-oriented programming in TypeScript with free flashcards and hands-on coding examples. This lesson covers classes, interfaces, inheritance, access modifiers, abstract classes, and advanced OOP patternsβessential concepts for building scalable, maintainable TypeScript applications.
π» Welcome to Object-Oriented TypeScript
Object-Oriented Programming (OOP) is a paradigm that organizes code around objects and classes rather than functions and logic. TypeScript enhances JavaScript's OOP capabilities with static typing, interfaces, and access modifiers, making your code more robust and self-documenting.
Whether you're building a web application, server-side API, or complex enterprise system, understanding OOP in TypeScript will help you write cleaner, more maintainable code. Let's dive into the core concepts that make TypeScript's OOP features so powerful! π
ποΈ Core Concepts
1οΈβ£ Classes and Objects
A class is a blueprint for creating objects. It encapsulates data (properties) and behavior (methods) into a single unit.
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string {
return `Hello, my name is ${this.name}`;
}
}
const person = new Person("Alice", 30);
console.log(person.greet()); // "Hello, my name is Alice"
Key Components:
- Properties:
nameandagestore data - Constructor: Special method that runs when creating a new instance
- Methods: Functions that define behavior (
greet) - Instance:
personis an object created from thePersonclass
π‘ Tip: TypeScript requires you to declare property types, unlike JavaScript. This catches errors at compile-time!
2οΈβ£ Access Modifiers
TypeScript provides three access modifiers to control visibility:
| Modifier | Accessible From | Use Case |
|---|---|---|
public |
Anywhere (default) | Public API, external access |
private |
Only within the class | Internal implementation details |
protected |
Class and subclasses | Shared with child classes |
class BankAccount {
public accountNumber: string;
private balance: number;
protected owner: string;
constructor(accountNumber: string, owner: string) {
this.accountNumber = accountNumber;
this.owner = owner;
this.balance = 0;
}
public deposit(amount: number): void {
this.balance += amount; // Can access private property internally
}
public getBalance(): number {
return this.balance;
}
}
const account = new BankAccount("12345", "Bob");
account.deposit(100);
console.log(account.getBalance()); // 100
// console.log(account.balance); // β Error: Property 'balance' is private
π§ Mnemonic: PPP - Public for everyone, Private for me, Protected for family (subclasses).
3οΈβ£ Interfaces
An interface defines a contract that classes must follow. It specifies what properties and methods a class must implement without providing implementation details.
interface Vehicle {
brand: string;
speed: number;
accelerate(): void;
brake(): void;
}
class Car implements Vehicle {
brand: string;
speed: number;
constructor(brand: string) {
this.brand = brand;
this.speed = 0;
}
accelerate(): void {
this.speed += 10;
}
brake(): void {
this.speed = Math.max(0, this.speed - 10);
}
}
const myCar = new Car("Toyota");
myCar.accelerate();
console.log(myCar.speed); // 10
Interfaces vs. Classes:
- Interfaces: Define structure only (no implementation)
- Classes: Provide both structure and implementation
- A class can implement multiple interfaces but extend only one class
π‘ Tip: Use interfaces for type checking and contracts. Use classes when you need actual object instances.
4οΈβ£ Inheritance and Extending Classes
Inheritance allows a class to inherit properties and methods from a parent class, promoting code reuse.
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number): void {
console.log(`${this.name} moved ${distance} meters`);
}
}
class Dog extends Animal {
bark(): void {
console.log(`${this.name} barks: Woof! Woof!`);
}
}
class Bird extends Animal {
fly(distance: number): void {
console.log(`${this.name} flew ${distance} meters`);
}
}
const dog = new Dog("Buddy");
dog.move(10); // "Buddy moved 10 meters"
dog.bark(); // "Buddy barks: Woof! Woof!"
const bird = new Bird("Tweety");
bird.move(5); // "Tweety moved 5 meters"
bird.fly(20); // "Tweety flew 20 meters"
Key Concepts:
- extends: Keyword to inherit from a parent class
- super: Keyword to call parent class constructor or methods
- Method Overriding: Child classes can override parent methods
5οΈβ£ Abstract Classes
Abstract classes cannot be instantiated directly. They serve as base classes that define common behavior for subclasses.
abstract class Shape {
abstract getArea(): number; // Must be implemented by subclasses
describe(): string {
return `This shape has an area of ${this.getArea()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
getArea(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
const circle = new Circle(5);
console.log(circle.describe()); // "This shape has an area of 78.54..."
const rect = new Rectangle(4, 6);
console.log(rect.describe()); // "This shape has an area of 24"
// const shape = new Shape(); // β Error: Cannot create instance of abstract class
Abstract vs. Interface:
- Abstract classes can have implementation (concrete methods)
- Interfaces have no implementation (pure contracts)
- A class can implement multiple interfaces but extend only one abstract class
6οΈβ£ Property Shorthand in Constructors
TypeScript provides a shorthand for declaring and initializing properties directly in the constructor:
// Long way
class Product {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
// Shorthand way
class Product {
constructor(public name: string, public price: number) {
// Properties automatically declared and initialized!
}
}
const item = new Product("Laptop", 999);
console.log(item.name); // "Laptop"
console.log(item.price); // 999
π‘ Tip: This shorthand works with public, private, protected, and readonly modifiers.
7οΈβ£ Getters and Setters
Getters and setters allow you to control how properties are accessed and modified:
class Temperature {
private _celsius: number = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) {
throw new Error("Temperature below absolute zero!");
}
this._celsius = value;
}
get fahrenheit(): number {
return (this._celsius * 9/5) + 32;
}
set fahrenheit(value: number) {
this._celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
temp.fahrenheit = 86;
console.log(temp.celsius); // 30
π§ Try this: Add validation in setters to ensure data integrity (e.g., non-negative prices, valid email formats).
8οΈβ£ Static Members
Static properties and methods belong to the class itself, not to instances:
class MathHelper {
static PI: number = 3.14159;
static calculateCircleArea(radius: number): number {
return this.PI * radius ** 2;
}
}
// Call without creating an instance
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.calculateCircleArea(5)); // 78.5398
Use Cases:
- Utility functions (e.g.,
Math.random(),Array.from()) - Constants shared across all instances
- Factory methods for creating instances
π Practical Examples
Example 1: E-Commerce System
Let's build a simple e-commerce system with products and shopping carts:
interface Priceable {
getPrice(): number;
}
class Product implements Priceable {
constructor(
public id: string,
public name: string,
private price: number,
public stock: number
) {}
getPrice(): number {
return this.price;
}
reduceStock(quantity: number): boolean {
if (this.stock >= quantity) {
this.stock -= quantity;
return true;
}
return false;
}
}
class DiscountedProduct extends Product {
constructor(
id: string,
name: string,
price: number,
stock: number,
private discountPercent: number
) {
super(id, name, price, stock);
}
getPrice(): number {
const originalPrice = super.getPrice();
return originalPrice * (1 - this.discountPercent / 100);
}
}
class ShoppingCart {
private items: Map<Product, number> = new Map();
addItem(product: Product, quantity: number): void {
const currentQty = this.items.get(product) || 0;
this.items.set(product, currentQty + quantity);
}
getTotalPrice(): number {
let total = 0;
this.items.forEach((quantity, product) => {
total += product.getPrice() * quantity;
});
return total;
}
checkout(): boolean {
for (const [product, quantity] of this.items) {
if (!product.reduceStock(quantity)) {
return false; // Not enough stock
}
}
this.items.clear();
return true;
}
}
// Usage
const laptop = new Product("L001", "Laptop", 999, 10);
const phone = new DiscountedProduct("P001", "Phone", 699, 20, 15);
const cart = new ShoppingCart();
cart.addItem(laptop, 1);
cart.addItem(phone, 2);
console.log(`Total: $${cart.getTotalPrice()}`); // Total: $2187.30
console.log(cart.checkout()); // true
Key Takeaways:
- Interface
Priceableensures all products have agetPrice()method - Inheritance allows
DiscountedProductto override pricing logic - Encapsulation keeps stock management internal to
Product - Polymorphism lets
ShoppingCartwork with anyPriceableitem
Example 2: User Authentication System
abstract class User {
constructor(
protected username: string,
private password: string
) {}
authenticate(inputPassword: string): boolean {
return this.password === inputPassword;
}
abstract getPermissions(): string[];
}
class AdminUser extends User {
getPermissions(): string[] {
return ["read", "write", "delete", "manage_users"];
}
}
class RegularUser extends User {
getPermissions(): string[] {
return ["read", "write"];
}
}
class GuestUser extends User {
getPermissions(): string[] {
return ["read"];
}
}
class AuthenticationService {
private users: User[] = [];
registerUser(user: User): void {
this.users.push(user);
}
login(username: string, password: string): User | null {
const user = this.users.find(u => u['username'] === username);
if (user && user.authenticate(password)) {
return user;
}
return null;
}
}
// Usage
const authService = new AuthenticationService();
const admin = new AdminUser("admin", "admin123");
const user = new RegularUser("john", "pass456");
authService.registerUser(admin);
authService.registerUser(user);
const loggedIn = authService.login("john", "pass456");
if (loggedIn) {
console.log(loggedIn.getPermissions()); // ["read", "write"]
}
Design Patterns Applied:
- Template Method Pattern: Abstract
Userclass defines authentication, subclasses define permissions - Polymorphism:
AuthenticationServiceworks with anyUsersubclass - Encapsulation: Password is private, accessible only through
authenticate()
Example 3: Game Character System
interface Movable {
move(x: number, y: number): void;
}
interface Attackable {
attack(target: Character): void;
}
abstract class Character implements Movable {
protected x: number = 0;
protected y: number = 0;
constructor(
public name: string,
protected health: number,
protected attackPower: number
) {}
move(x: number, y: number): void {
this.x = x;
this.y = y;
console.log(`${this.name} moved to (${x}, ${y})`);
}
takeDamage(amount: number): void {
this.health = Math.max(0, this.health - amount);
if (this.health === 0) {
console.log(`${this.name} has been defeated!`);
}
}
isAlive(): boolean {
return this.health > 0;
}
abstract specialAbility(): void;
}
class Warrior extends Character implements Attackable {
attack(target: Character): void {
console.log(`${this.name} attacks ${target.name}!`);
target.takeDamage(this.attackPower);
}
specialAbility(): void {
console.log(`${this.name} uses Shield Bash!`);
this.attackPower *= 1.5;
}
}
class Mage extends Character implements Attackable {
private mana: number = 100;
attack(target: Character): void {
if (this.mana >= 10) {
console.log(`${this.name} casts Fireball at ${target.name}!`);
target.takeDamage(this.attackPower * 2);
this.mana -= 10;
} else {
console.log(`${this.name} is out of mana!`);
}
}
specialAbility(): void {
console.log(`${this.name} meditates and restores mana`);
this.mana = 100;
}
}
// Usage
const warrior = new Warrior("Conan", 100, 20);
const mage = new Mage("Gandalf", 80, 15);
warrior.move(10, 5);
mage.move(12, 6);
warrior.attack(mage); // Conan attacks Gandalf!
mage.attack(warrior); // Gandalf casts Fireball at Conan!
warrior.specialAbility();
mage.specialAbility();
OOP Principles Demonstrated:
- Abstraction:
Characterhides implementation details - Inheritance:
WarriorandMageshare common character traits - Polymorphism: Both implement
Attackablebut with different behavior - Encapsulation: Health and mana are protected/private
Example 4: Event System with Observers
interface Observer {
update(message: string): void;
}
class Subject {
private observers: Observer[] = [];
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(message: string): void {
for (const observer of this.observers) {
observer.update(message);
}
}
}
class EmailNotifier implements Observer {
constructor(private email: string) {}
update(message: string): void {
console.log(`Email to ${this.email}: ${message}`);
}
}
class SMSNotifier implements Observer {
constructor(private phoneNumber: string) {}
update(message: string): void {
console.log(`SMS to ${this.phoneNumber}: ${message}`);
}
}
class OrderSystem extends Subject {
placeOrder(orderId: string): void {
console.log(`Order ${orderId} placed`);
this.notify(`Your order ${orderId} has been confirmed`);
}
shipOrder(orderId: string): void {
console.log(`Order ${orderId} shipped`);
this.notify(`Your order ${orderId} is on the way`);
}
}
// Usage
const orderSystem = new OrderSystem();
const emailNotif = new EmailNotifier("user@example.com");
const smsNotif = new SMSNotifier("+1234567890");
orderSystem.attach(emailNotif);
orderSystem.attach(smsNotif);
orderSystem.placeOrder("ORD-001");
// Output:
// Order ORD-001 placed
// Email to user@example.com: Your order ORD-001 has been confirmed
// SMS to +1234567890: Your order ORD-001 has been confirmed
Pattern: Observer Pattern - subjects notify observers of state changes without tight coupling.
β οΈ Common Mistakes
1. Forgetting to Call super() in Subclass Constructor
// β WRONG
class Dog extends Animal {
constructor(name: string, breed: string) {
// Error: Must call super() before accessing 'this'
this.name = name;
}
}
// β
RIGHT
class Dog extends Animal {
constructor(name: string, private breed: string) {
super(name); // Call parent constructor first!
}
}
2. Misusing Access Modifiers
// β WRONG
class User {
private password: string;
constructor(password: string) {
this.password = password;
}
}
class AdminUser extends User {
showPassword(): void {
console.log(this.password); // Error: 'password' is private
}
}
// β
RIGHT
class User {
protected password: string; // Use protected for subclass access
}
3. Not Implementing All Interface Members
// β WRONG
interface Drawable {
draw(): void;
clear(): void;
}
class Circle implements Drawable {
draw(): void {
console.log("Drawing circle");
}
// Error: Class 'Circle' incorrectly implements interface 'Drawable'
// Property 'clear' is missing
}
// β
RIGHT
class Circle implements Drawable {
draw(): void {
console.log("Drawing circle");
}
clear(): void {
console.log("Clearing circle");
}
}
4. Trying to Instantiate Abstract Classes
// β WRONG
abstract class Vehicle {
abstract move(): void;
}
const vehicle = new Vehicle(); // Error: Cannot create instance
// β
RIGHT
class Car extends Vehicle {
move(): void {
console.log("Driving");
}
}
const car = new Car();
5. Confusing this Context in Methods
// β WRONG
class Counter {
count = 0;
increment() {
this.count++;
}
}
const counter = new Counter();
const inc = counter.increment;
inc(); // Error: 'this' is undefined or refers to wrong object
// β
RIGHT (use arrow function or bind)
class Counter {
count = 0;
increment = () => {
this.count++; // Arrow function preserves 'this'
}
}
6. Overusing Inheritance Instead of Composition
// β WRONG (Deep inheritance hierarchy)
class Vehicle {}
class LandVehicle extends Vehicle {}
class Car extends LandVehicle {}
class SportsCar extends Car {}
class RacingCar extends SportsCar {} // Too deep!
// β
RIGHT (Prefer composition)
interface Engine {
start(): void;
}
class StandardEngine implements Engine {
start(): void { console.log("Standard engine starting"); }
}
class TurboEngine implements Engine {
start(): void { console.log("Turbo engine starting"); }
}
class Car {
constructor(private engine: Engine) {}
start(): void {
this.engine.start();
}
}
π‘ Remember: "Favor composition over inheritance" - it's more flexible and easier to maintain.
π― Key Takeaways
β Classes encapsulate data and behavior into reusable blueprints
β
Access modifiers (public, private, protected) control visibility and encapsulation
β Interfaces define contracts without implementation; classes provide the implementation
β
Inheritance (extends) promotes code reuse by creating parent-child relationships
β Abstract classes provide partial implementation and force subclasses to complete it
β Polymorphism allows different classes to be used interchangeably through interfaces or base classes
β Getters/setters control property access and add validation logic
β Static members belong to the class itself, not instances
β Constructor shorthand (parameter properties) reduces boilerplate code
β Composition over inheritance often leads to more maintainable code
π€ Did you know? TypeScript's structural type system means that if two classes have the same structure, they're compatible even without explicit inheritance!
π Further Study
- TypeScript Handbook - Classes: https://www.typescriptlang.org/docs/handbook/2/classes.html
- MDN - Object-Oriented Programming: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_programming
- Refactoring Guru - Design Patterns: https://refactoring.guru/design-patterns/typescript
π Quick Reference Card
| Concept | Syntax | Purpose |
|---|---|---|
| Class Declaration | class MyClass { } | Define a blueprint for objects |
| Constructor | constructor(param: type) { } | Initialize new instances |
| Public Property | public name: string; | Accessible everywhere |
| Private Property | private id: number; | Only within class |
| Protected Property | protected data: any; | Class and subclasses |
| Interface | interface IName { } | Define contract/structure |
| Implements | class X implements Y { } | Fulfill interface contract |
| Extends | class Child extends Parent { } | Inherit from parent class |
| Abstract Class | abstract class Base { } | Cannot instantiate; partial implementation |
| Abstract Method | abstract method(): type; | Must be implemented by subclass |
| Static Member | static prop: type; | Belongs to class, not instance |
| Getter | get prop(): type { } | Computed property access |
| Setter | set prop(value: type) { } | Controlled property modification |
| Readonly | readonly prop: type; | Cannot be changed after initialization |
| Super Call | super() | Call parent constructor/method |
π§ Final Mnemonic: "C.I.E.A.P.S" - Classes define structure, Interfaces define contracts, Extends for inheritance, Abstract for partial implementation, Private for encapsulation, Static for shared utilities.